Add files using upload-large-folder tool
Browse files- .gitattributes +1 -34
- .gitignore +6 -0
- LICENSE +23 -0
- README.md +288 -0
- SHA256SUMS +31 -0
- THIRD_PARTY_NOTICES.md +39 -0
- benchmark_results.json +142 -0
- example.py +89 -0
- nvfp4_config.json +0 -0
- runtime/chatterbox-flash/.gitignore +30 -0
- runtime/chatterbox-flash/LICENSE +21 -0
- runtime/chatterbox-flash/README.md +11 -0
- runtime/chatterbox-flash/chatterbox_flash/__init__.py +36 -0
- runtime/chatterbox-flash/chatterbox_flash/attention_flashinfer.py +20 -0
- runtime/chatterbox-flash/chatterbox_flash/calibration.py +82 -0
- runtime/chatterbox-flash/chatterbox_flash/cfg_guidance.py +57 -0
- runtime/chatterbox-flash/chatterbox_flash/engines/__init__.py +168 -0
- runtime/chatterbox-flash/chatterbox_flash/engines/base.py +141 -0
- runtime/chatterbox-flash/chatterbox_flash/engines/flashinfer.py +1656 -0
- runtime/chatterbox-flash/chatterbox_flash/engines/mlx.py +621 -0
- runtime/chatterbox-flash/chatterbox_flash/engines/torch_sdpa.py +372 -0
- runtime/chatterbox-flash/chatterbox_flash/model.py +936 -0
- runtime/chatterbox-flash/chatterbox_flash/nvfp4.py +709 -0
- runtime/chatterbox-flash/chatterbox_flash/text_norm/__init__.py +15 -0
- runtime/chatterbox-flash/chatterbox_flash/text_norm/cleaners.py +129 -0
- runtime/chatterbox-flash/chatterbox_flash/text_norm/number_norm.py +366 -0
- runtime/chatterbox-flash/chatterbox_flash/text_norm/phone_norm.py +107 -0
- runtime/chatterbox-flash/chatterbox_flash/text_norm/time_norm.py +49 -0
- runtime/chatterbox-flash/chatterbox_flash/tts.py +398 -0
- runtime/chatterbox-flash/pyproject.toml +137 -0
- runtime/chatterbox-flash/uv.lock +0 -0
- t3_nvfp4.safetensors +3 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.cache/
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.py[cod]
|
| 5 |
+
output.wav
|
| 6 |
+
|
LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Resemble AI
|
| 4 |
+
Copyright (c) 2026 Chatterbox-Flash NVFP4 contributors
|
| 5 |
+
|
| 6 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 8 |
+
in the Software without restriction, including without limitation the rights
|
| 9 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 10 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 11 |
+
furnished to do so, subject to the following conditions:
|
| 12 |
+
|
| 13 |
+
The above copyright notice and this permission notice shall be included in all
|
| 14 |
+
copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 22 |
+
SOFTWARE.
|
| 23 |
+
|
README.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
library_name: chatterbox-flash
|
| 6 |
+
pipeline_tag: text-to-speech
|
| 7 |
+
base_model: ResembleAI/chatterbox-flash
|
| 8 |
+
base_model_relation: quantized
|
| 9 |
+
inference: false
|
| 10 |
+
tags:
|
| 11 |
+
- text-to-speech
|
| 12 |
+
- zero-shot-tts
|
| 13 |
+
- voice-cloning
|
| 14 |
+
- chatterbox-flash
|
| 15 |
+
- nvfp4
|
| 16 |
+
- w4a16
|
| 17 |
+
- quantized
|
| 18 |
+
- nvidia
|
| 19 |
+
- blackwell
|
| 20 |
+
- flashinfer
|
| 21 |
+
- safetensors
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
# Chatterbox-Flash T3 W4A16 NVFP4
|
| 25 |
+
|
| 26 |
+
This is an unofficial community NVFP4 derivative of
|
| 27 |
+
[ResembleAI/chatterbox-flash](https://huggingface.co/ResembleAI/chatterbox-flash).
|
| 28 |
+
It is not affiliated with or endorsed by Resemble AI or NVIDIA.
|
| 29 |
+
|
| 30 |
+
The package replaces all 120 Chatterbox-Flash T3 transformer projection
|
| 31 |
+
operators with source-free NVFP4 weights. Weights use native FP4 (E2M1) with
|
| 32 |
+
E4M3 block scales; activations remain BF16, so the precise description is
|
| 33 |
+
**T3 all-projection W4A16 NVFP4**. Embeddings, norms, conditioning layers,
|
| 34 |
+
speech head, S3Gen, and the voice encoder remain at their upstream precision.
|
| 35 |
+
This is not a whole-pipeline 4-bit model.
|
| 36 |
+
|
| 37 |
+
The canonical artifact does not contain the original BF16 T3 projection
|
| 38 |
+
weights. Its custom loader constructs those parameters on the meta device,
|
| 39 |
+
prepares the canonical FP4 codes/scales directly for FlashInfer, and never
|
| 40 |
+
calls `nvfp4_quantize` at load time. Engine rebuilds reuse the model-attached
|
| 41 |
+
projection provider.
|
| 42 |
+
|
| 43 |
+
## What this release actually improves
|
| 44 |
+
|
| 45 |
+
The primary benefit is **much lower storage and VRAM with a modest speed gain**,
|
| 46 |
+
not a claim that NVFP4 makes the complete TTS pipeline dramatically faster.
|
| 47 |
+
S3Gen and several small T3 operations remain high precision, and some
|
| 48 |
+
irregular prefix projections are slower in FP4 than BF16.
|
| 49 |
+
|
| 50 |
+
On the tested RTX 5060 Ti, the recommended D16 quality profile delivered:
|
| 51 |
+
|
| 52 |
+
- **83.97% smaller T3 checkpoint**: 341 MB instead of 2.13 GB.
|
| 53 |
+
- **55.88% less complete model-asset storage/download** after adding the
|
| 54 |
+
unchanged S3Gen, voice encoder, and tokenizer: 1.412 GB instead of 3.200 GB.
|
| 55 |
+
- **33.2% lower allocated VRAM immediately after full pipeline load**:
|
| 56 |
+
1.444 GB instead of 2.162 GB.
|
| 57 |
+
- **18.82% lower maximum allocated VRAM in the natural eight-text suite**:
|
| 58 |
+
5.215 GB instead of 6.424 GB. Fixed-work peak allocation was 34.09% lower.
|
| 59 |
+
- **1.0811x aggregate-RTF speedup** (7.50% lower duration-normalized latency)
|
| 60 |
+
and **1.1131x valid-token T3 throughput**. The fixed-work T3 speedup was
|
| 61 |
+
1.1260x; fixed whole-pipeline speedup was 1.0631x.
|
| 62 |
+
|
| 63 |
+
The D24 fast profile reaches 25.13% lower RTF than the D16 BF16 baseline, but
|
| 64 |
+
that is a combined block-size-plus-NVFP4 result. Against matched D24 BF16,
|
| 65 |
+
NVFP4 alone provides only a 1.0309x RTF speedup. This distinction matters when
|
| 66 |
+
deciding whether the model is useful for your deployment.
|
| 67 |
+
|
| 68 |
+
## How quality was evaluated
|
| 69 |
+
|
| 70 |
+
A Gemma Audio evaluator listened to eight English test utterances generated
|
| 71 |
+
from one zero-shot reference voice. It transcribed each output, scored
|
| 72 |
+
intelligibility, pronunciation, voice similarity, naturalness, and artifacts,
|
| 73 |
+
and flagged hard failures. BF16 and NVFP4 were also compared blindly in both
|
| 74 |
+
A/B orders to expose position bias.
|
| 75 |
+
|
| 76 |
+
- D16 BF16/NVFP4 means were **94.125/94.625**. Both produced exact transcripts
|
| 77 |
+
for 8/8 texts with zero hard failures. After reversing A/B order, the
|
| 78 |
+
per-case result resolved to seven ties and one consistent BF16 preference.
|
| 79 |
+
- D24 BF16/NVFP4 means were **95.0/93.125**. Both produced exact transcripts
|
| 80 |
+
for 8/8 texts with zero hard failures. Order control resolved to six ties,
|
| 81 |
+
one BF16 preference, and one NVFP4 preference.
|
| 82 |
+
|
| 83 |
+
This is encouraging automated evidence, not human MOS, a standard Seed-TTS
|
| 84 |
+
benchmark, proof of perceptual identity, or a multi-speaker evaluation. The
|
| 85 |
+
audio was intentionally not published because consent for public
|
| 86 |
+
redistribution of the reference voice was not established.
|
| 87 |
+
|
| 88 |
+
## Hardware and software
|
| 89 |
+
|
| 90 |
+
The accepted release configuration is intentionally narrow:
|
| 91 |
+
|
| 92 |
+
- Linux x86-64 and Python 3.12
|
| 93 |
+
- NVIDIA RTX 50-series SM120/SM121 (tested on RTX 5060 Ti)
|
| 94 |
+
- PyTorch 2.7.1+cu128 and CUDA 12.8 wheels
|
| 95 |
+
- FlashInfer 0.6.14 with `nvidia-cutlass-dsl` 4.5.2
|
| 96 |
+
- BF16 activations, FlashInfer CUDA graphs, user batch size 1
|
| 97 |
+
|
| 98 |
+
There is no CPU, MLX, Torch-SDPA, older-GPU, or non-NVFP4 fallback because the
|
| 99 |
+
BF16 projection sources are deliberately absent. RTX 5080 and SM121 are
|
| 100 |
+
allowed by the loader but have not yet been benchmarked. This is custom code,
|
| 101 |
+
not a Transformers `AutoModel` checkpoint or a standard Hub quantization
|
| 102 |
+
format, so the hosted inference widget is disabled.
|
| 103 |
+
|
| 104 |
+
`nvidia-cutlass-dsl` is installed under NVIDIA's separate software license.
|
| 105 |
+
Review [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) before installation.
|
| 106 |
+
|
| 107 |
+
## Install
|
| 108 |
+
|
| 109 |
+
Clone the model repository, then use the included lock. Plain `pip install`
|
| 110 |
+
does not honor the upstream Torch override or the official cu128 wheel source.
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
git clone https://huggingface.co/ajh-code/chatterbox-flash-t3-w4a16-nvfp4
|
| 114 |
+
cd chatterbox-flash-t3-w4a16-nvfp4
|
| 115 |
+
|
| 116 |
+
uv sync \
|
| 117 |
+
--project runtime/chatterbox-flash \
|
| 118 |
+
--locked \
|
| 119 |
+
--extra flashinfer \
|
| 120 |
+
--python 3.12
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
The lock resolves the exact validated critical stack. A dry verification is:
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
runtime/chatterbox-flash/.venv/bin/python - <<'PY'
|
| 127 |
+
import importlib.metadata as md
|
| 128 |
+
import flashinfer
|
| 129 |
+
import torch
|
| 130 |
+
|
| 131 |
+
assert torch.__version__ == "2.7.1+cu128"
|
| 132 |
+
assert torch.version.cuda == "12.8"
|
| 133 |
+
assert md.version("flashinfer-python") == "0.6.14"
|
| 134 |
+
assert md.version("nvidia-cutlass-dsl") == "4.5.2"
|
| 135 |
+
assert hasattr(flashinfer, "prepare_bf16_fp4_weights")
|
| 136 |
+
assert hasattr(flashinfer, "mm_bf16_fp4")
|
| 137 |
+
assert torch.cuda.get_device_capability() in {(12, 0), (12, 1)}
|
| 138 |
+
print(torch.cuda.get_device_name(), "is ready")
|
| 139 |
+
PY
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Zero-shot voice cloning
|
| 143 |
+
|
| 144 |
+
Use only reference audio that you have permission to clone.
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
runtime/chatterbox-flash/.venv/bin/python example.py \
|
| 148 |
+
--prompt-wav your-reference.wav \
|
| 149 |
+
--text "This is Chatterbox Flash running native NVFP4 projections." \
|
| 150 |
+
--output output.wav \
|
| 151 |
+
--block-size 16
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
The example downloads only the unchanged S3Gen, voice-encoder, and tokenizer
|
| 155 |
+
assets from the immutable base revision
|
| 156 |
+
`4385507288b8197e6dab8b4e6b1603328d549d9d`. It verifies their sizes and
|
| 157 |
+
SHA-256 fingerprints from `nvfp4_config.json`. It never downloads the original
|
| 158 |
+
2.13 GB `t3_flash.safetensors`.
|
| 159 |
+
|
| 160 |
+
The first decode for a new GPU/exact block shape invokes FlashInfer setup and
|
| 161 |
+
tuning. On the RTX 5060 Ti, a fresh process with the selected D16 tactic already
|
| 162 |
+
cached took 10.57 seconds for the first 1.28-second utterance versus 0.154
|
| 163 |
+
seconds for the same-process warm repeat. A completely missing tactic has taken
|
| 164 |
+
roughly 25 seconds in development. Autotune results are GPU-specific, so this
|
| 165 |
+
repository does not ship a universal cache.
|
| 166 |
+
|
| 167 |
+
## Profiles
|
| 168 |
+
|
| 169 |
+
`D=16` is the quality-default profile. `D=24` is an explicitly optional fast
|
| 170 |
+
profile: it is materially faster overall but had a slightly lower automated
|
| 171 |
+
mean score and less consistent pairwise preference. `D=32` was tested and
|
| 172 |
+
rejected after token-cap failures and an OOM in the natural suite; its cached
|
| 173 |
+
prior remains serialized for research provenance, but the release loader
|
| 174 |
+
refuses it.
|
| 175 |
+
|
| 176 |
+
| Profile | Intended use | NVFP4 aggregate RTF | NVFP4 T3 tok/s | Automated mean |
|
| 177 |
+
|---|---|---:|---:|---:|
|
| 178 |
+
| D16 | Quality default | 0.08725 | 365.4 | 94.625 |
|
| 179 |
+
| D24 | Optional fast | 0.07062 | 483.3 | 93.125 |
|
| 180 |
+
|
| 181 |
+
## RTX 5060 Ti results
|
| 182 |
+
|
| 183 |
+
The natural test is an eight-text, one-reference suite. Pipeline and T3 times
|
| 184 |
+
are sums of per-case medians; aggregate RTF divides summed pipeline time by
|
| 185 |
+
summed generated-audio duration. Memory is the maximum per-case median
|
| 186 |
+
PyTorch CUDA peak, not total `nvidia-smi` process memory.
|
| 187 |
+
|
| 188 |
+
| Profile | Comparison | RTF speedup | T3 throughput | Peak allocated reduction |
|
| 189 |
+
|---|---|---:|---:|---:|
|
| 190 |
+
| D16 | W4A16 vs matched BF16 | 1.0811x | 1.1131x | 18.82% / 1.209 GB |
|
| 191 |
+
| D24 | W4A16 vs matched BF16 | 1.0309x | 1.0450x | 21.57% / 1.410 GB |
|
| 192 |
+
|
| 193 |
+
A fixed capped-work control removes sampled-length attribution. D16 T3 is
|
| 194 |
+
`1.1260x` faster and D24 T3 is `1.0656x` faster; each reduces fixed-work peak
|
| 195 |
+
allocated memory by about 34%. D16 fixed whole-pipeline speedup is `1.0631x`.
|
| 196 |
+
|
| 197 |
+
Do not quote the larger raw natural-suite wall-time ratios as pure NVFP4
|
| 198 |
+
speedups: the BF16 and W4A16 models sampled different token counts. The
|
| 199 |
+
D24-versus-D16 speed difference is primarily the larger block size, not FP4
|
| 200 |
+
alone. Complete numeric records and caveats are in
|
| 201 |
+
[benchmark_results.json](benchmark_results.json).
|
| 202 |
+
|
| 203 |
+
Automated quality was close but is not proof of perceptual identity:
|
| 204 |
+
|
| 205 |
+
- D16 BF16/NVFP4 means were 94.125/94.625; both transcribed 8/8 exactly with
|
| 206 |
+
zero hard failures. Order-controlled pairwise scoring resolved to seven ties
|
| 207 |
+
and one BF16 win.
|
| 208 |
+
- D24 BF16/NVFP4 means were 95.0/93.125; both transcribed 8/8 exactly with zero
|
| 209 |
+
hard failures. Order control resolved to six ties, one BF16 win, and one
|
| 210 |
+
NVFP4 win.
|
| 211 |
+
|
| 212 |
+
This was not human MOS, a standard Seed-TTS evaluation, or a multi-speaker
|
| 213 |
+
release study.
|
| 214 |
+
|
| 215 |
+
## Artifact and load behavior
|
| 216 |
+
|
| 217 |
+
- `t3_nvfp4.safetensors`: 341,439,040 bytes, 447 tensors, SHA-256
|
| 218 |
+
`c5ea2d1d1441f657b383d8dad54bbae3778447d675822fb15918cb99b3587f1e`
|
| 219 |
+
- 360 canonical NVFP4 tensors cover 30 layers × 4 fused projection kinds ×
|
| 220 |
+
FP4 codes/E4M3 scales/FP32 alpha.
|
| 221 |
+
- 82 non-projection T3 tensors remain BF16; three fixed priors and two rotary
|
| 222 |
+
buffers support source-free construction.
|
| 223 |
+
- Original T3 checkpoint: 2,129,657,904 bytes. The derivative T3 file is
|
| 224 |
+
83.97% smaller.
|
| 225 |
+
- Derivative T3 plus the three unchanged downloaded base assets totals
|
| 226 |
+
1,412,035,402 bytes versus 3,200,254,266 bytes upstream: 55.88% less model
|
| 227 |
+
asset storage/download, excluding small runtime files and cache.
|
| 228 |
+
|
| 229 |
+
In the final source-free release smoke, fresh-process pipeline load with base
|
| 230 |
+
hash verification and warm JIT state took 2.675 seconds and settled at 1.444 GB
|
| 231 |
+
PyTorch allocated / 1.478 GB reserved before the first generation. It prepared
|
| 232 |
+
120 projections into 283,116,000 bytes, without materializing BF16 projection
|
| 233 |
+
weights or calling the quantizer. The short generation peaked at 2.548 GB
|
| 234 |
+
allocated. These are one-machine diagnostics; first-generation setup is
|
| 235 |
+
reported separately above.
|
| 236 |
+
|
| 237 |
+
The canonical D16 loader regression reproduced the accepted stripped runtime
|
| 238 |
+
bit-for-bit: token history, waveform tensor SHA-256, and saved WAV SHA-256 all
|
| 239 |
+
matched. A longer request also rebuilt the engine successfully while retaining
|
| 240 |
+
the same source-free projection provider.
|
| 241 |
+
|
| 242 |
+
## Limitations and responsible use
|
| 243 |
+
|
| 244 |
+
- Only RTX 5060 Ti / SM120, batch one, and the documented settings received the
|
| 245 |
+
complete technical and small automated-audio gate.
|
| 246 |
+
- D16 is the only quality-default profile. D24 is optional; D32 is rejected.
|
| 247 |
+
- S3Gen and voice-encoder execution are not quantized by this artifact.
|
| 248 |
+
- Prefix projections can remain slower than BF16 at irregular shapes; the
|
| 249 |
+
decode block projections provide the measured NVFP4 gain.
|
| 250 |
+
- Streaming, multi-request throughput, RTX 5080 tuning, and human multi-voice
|
| 251 |
+
listening remain future work.
|
| 252 |
+
- Generated length can change after quantization, affecting latency, memory,
|
| 253 |
+
pauses, and termination even when transcripts are correct.
|
| 254 |
+
- Voice cloning can enable impersonation or deception. Obtain the speaker's
|
| 255 |
+
consent, disclose synthetic audio where appropriate, and comply with local
|
| 256 |
+
law and platform policies.
|
| 257 |
+
|
| 258 |
+
No evaluation audio is included because the reference clip's redistribution
|
| 259 |
+
and voice-consent status was not established for a public model repository.
|
| 260 |
+
|
| 261 |
+
## Attribution and citation
|
| 262 |
+
|
| 263 |
+
Chatterbox-Flash model weights and vendored runtime code are Copyright (c) 2026
|
| 264 |
+
Resemble AI and licensed under MIT. This runtime is based on source commit
|
| 265 |
+
`74e05baa8ce574bf2cc571702391a21f1b0d48c5`; the derivative modifies the
|
| 266 |
+
FlashInfer projection path and adds the canonical NVFP4 loader. See
|
| 267 |
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
| 268 |
+
|
| 269 |
+
```bibtex
|
| 270 |
+
@misc{seo2026chatterboxflashpriorcalibratedblockdiffusion,
|
| 271 |
+
title={Chatterbox-Flash: Prior-Calibrated Block Diffusion for Streaming Zero-Shot TTS},
|
| 272 |
+
author={Deokjin Seo and Gangin Park and Kihyun Nam},
|
| 273 |
+
year={2026},
|
| 274 |
+
eprint={2605.30748},
|
| 275 |
+
archivePrefix={arXiv},
|
| 276 |
+
primaryClass={cs.SD},
|
| 277 |
+
url={https://arxiv.org/abs/2605.30748}
|
| 278 |
+
}
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
## Repository files
|
| 282 |
+
|
| 283 |
+
- `t3_nvfp4.safetensors`: canonical source-free checkpoint
|
| 284 |
+
- `nvfp4_config.json`: complete schema, fingerprints, revisions, and profiles
|
| 285 |
+
- `runtime/chatterbox-flash/`: pinned installable derivative runtime and lock
|
| 286 |
+
- `example.py`: tested local-bundle zero-shot generation entry point
|
| 287 |
+
- `benchmark_results.json`: machine-readable release measurements
|
| 288 |
+
- `SHA256SUMS`: release integrity manifest
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
d5fadc36773f0a6e03ae542aac79a3747abd47f77028ab304cb878dc2beb3ec1 ./.gitattributes
|
| 2 |
+
672ba8b147a82d1ecc3f9f1dc899d69986efb2f7e4fd9f939ce9f38948922b41 ./.gitignore
|
| 3 |
+
2eba8e98035070234c35a91bb6e3c4185505cf073ff2879c8ce5fc1a462f5534 ./LICENSE
|
| 4 |
+
c3c8b178f6fca0debb7e7072c6fce42376f922caee993d50c9fb36285ed60aea ./README.md
|
| 5 |
+
6a8b7141a4bb5ac41462c6e4c0547bd6ca701aef28fa5c4453d191658193a485 ./THIRD_PARTY_NOTICES.md
|
| 6 |
+
aaf7a6043f9de80a4a9ad99082274310d2dbfa9d6024753dc106755327bc0c75 ./benchmark_results.json
|
| 7 |
+
ff1be650be410c64a7e7d9bc2288c40638aed4af7383cbc560e77c4e87de38be ./example.py
|
| 8 |
+
7521540282c2b716c3135a768b810b8f39c01ab5f773b0f8c82c9f9d4f8c44b5 ./nvfp4_config.json
|
| 9 |
+
d6baef68ab47dd53741421f0440d62fc5632e7955c33d9c08b7b33a6c164b8c2 ./runtime/chatterbox-flash/.gitignore
|
| 10 |
+
a80ea1b21580d334f010acf2f1c055d57d0959b1e6f578cc2b0a3de2eb96a8bb ./runtime/chatterbox-flash/LICENSE
|
| 11 |
+
d41a6f6cdedb1284e64119630d61aba97bbe325f9a0a6e0c6cbeb8ba9057bcb4 ./runtime/chatterbox-flash/README.md
|
| 12 |
+
d1047075186b8f28646977a5e70e70f37a00abbd6d7ff5d77aa56a1cfd8a0697 ./runtime/chatterbox-flash/chatterbox_flash/__init__.py
|
| 13 |
+
d3676e229c668a2d12e9a08d8abf9990203f6f6664f2fb97d3bfa75494549a75 ./runtime/chatterbox-flash/chatterbox_flash/attention_flashinfer.py
|
| 14 |
+
6c7769bb30d846985751513a97be0d962ed378a82dbef124beb2d7b422a38991 ./runtime/chatterbox-flash/chatterbox_flash/calibration.py
|
| 15 |
+
e8703a7bf949f6e8d119962fcf84775a3b62169cd0d77c4e99c0a2cc85089858 ./runtime/chatterbox-flash/chatterbox_flash/cfg_guidance.py
|
| 16 |
+
f77672b56d510ef9d78f03d64ff1e192ef341b57f114f94d907bf8d49aad5cfe ./runtime/chatterbox-flash/chatterbox_flash/engines/__init__.py
|
| 17 |
+
0db90fd941bde83a834e5a3600806cfa8cab1fdb36c372de0dbfe95fe1f8dbd2 ./runtime/chatterbox-flash/chatterbox_flash/engines/base.py
|
| 18 |
+
177d1d750b56e4922503c4b20b8ffb7bb04965effafccdcf52a0af56cc4a79f4 ./runtime/chatterbox-flash/chatterbox_flash/engines/flashinfer.py
|
| 19 |
+
7ae970bfb9342a521bdf83576c736f6c8da5fd6324f5d54a13e053c20e4ddcd6 ./runtime/chatterbox-flash/chatterbox_flash/engines/mlx.py
|
| 20 |
+
b03799ffd9aad68eb33700fcfdeea490e999c884092687b9a16d877a334ec3ca ./runtime/chatterbox-flash/chatterbox_flash/engines/torch_sdpa.py
|
| 21 |
+
a6e51fb3237d6b5569749fdce66bea1056bdccdc4245ccc28bbeb5646ff314c3 ./runtime/chatterbox-flash/chatterbox_flash/model.py
|
| 22 |
+
276d1bc84f5c6c911fbe2b7b52097c015ae4e838bee2759acfc9ddebf4d68560 ./runtime/chatterbox-flash/chatterbox_flash/nvfp4.py
|
| 23 |
+
561e11514455dbab23bfcff7b7e9d8252c8aeedd0b36e1e906f585e5981220fa ./runtime/chatterbox-flash/chatterbox_flash/text_norm/__init__.py
|
| 24 |
+
4b221a4829756695224bc62b2b4f0c1993f40311f21f299ca5d98e14feb6dee3 ./runtime/chatterbox-flash/chatterbox_flash/text_norm/cleaners.py
|
| 25 |
+
1ffc6ea6d4649ee7498d1e3e184442b660811c2765cdd250d7e6c7021fa595ae ./runtime/chatterbox-flash/chatterbox_flash/text_norm/number_norm.py
|
| 26 |
+
26e26739d80282692aeecc9c44c7c0c83a116e1dcfedbddb370e066094e56d24 ./runtime/chatterbox-flash/chatterbox_flash/text_norm/phone_norm.py
|
| 27 |
+
a4254d1b82781a79c18902d5151ee35e3c871926a72e87cb5b1562843b5bb055 ./runtime/chatterbox-flash/chatterbox_flash/text_norm/time_norm.py
|
| 28 |
+
0c0665d0c1d9593422eb2c1bcb02f1ed2ce6bf6aed45729e10311525f985b57d ./runtime/chatterbox-flash/chatterbox_flash/tts.py
|
| 29 |
+
3ed7cf63c7fb04f46b453b2b60a2a890d1c64eac7945dabeea19aef6b40585d1 ./runtime/chatterbox-flash/pyproject.toml
|
| 30 |
+
763867108090620985de8aa8f3019f0eca356ffe37519b1d45ae56e1fdcc78b4 ./runtime/chatterbox-flash/uv.lock
|
| 31 |
+
c5ea2d1d1441f657b383d8dad54bbae3778447d675822fb15918cb99b3587f1e ./t3_nvfp4.safetensors
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
This bundle is an unofficial community derivative. It is not affiliated with
|
| 4 |
+
or endorsed by Resemble AI or NVIDIA.
|
| 5 |
+
|
| 6 |
+
## Chatterbox-Flash
|
| 7 |
+
|
| 8 |
+
The model weights and vendored runtime are derived from
|
| 9 |
+
[ResembleAI/chatterbox-flash](https://github.com/resemble-ai/chatterbox-flash),
|
| 10 |
+
source commit `74e05baa8ce574bf2cc571702391a21f1b0d48c5`.
|
| 11 |
+
|
| 12 |
+
Copyright (c) 2026 Resemble AI. Licensed under the MIT License. The complete
|
| 13 |
+
license text is reproduced in [LICENSE](LICENSE) and in
|
| 14 |
+
`runtime/chatterbox-flash/LICENSE`.
|
| 15 |
+
|
| 16 |
+
The derivative changes the FlashInfer engine/provider integration and adds a
|
| 17 |
+
canonical, source-free NVFP4 loader. The unchanged S3Gen, voice-encoder, and
|
| 18 |
+
tokenizer assets are not redistributed; the loader fetches them from the
|
| 19 |
+
immutable Chatterbox-Flash Hub revision recorded in `nvfp4_config.json`.
|
| 20 |
+
|
| 21 |
+
## Chatterbox-TTS
|
| 22 |
+
|
| 23 |
+
Chatterbox-Flash uses architecture and runtime components from
|
| 24 |
+
[ResembleAI/chatterbox](https://github.com/resemble-ai/chatterbox).
|
| 25 |
+
|
| 26 |
+
Copyright (c) 2025 Resemble AI. Licensed under the MIT License.
|
| 27 |
+
|
| 28 |
+
## Runtime dependencies
|
| 29 |
+
|
| 30 |
+
[FlashInfer](https://github.com/flashinfer-ai/flashinfer) is an Apache-2.0
|
| 31 |
+
runtime dependency and is not vendored.
|
| 32 |
+
|
| 33 |
+
`nvidia-cutlass-dsl` and `nvidia-cutlass-dsl-libs-base` are installed from
|
| 34 |
+
their official Python packages and are not vendored. They are distributed
|
| 35 |
+
under the NVIDIA Software License Agreement included in those packages. Users
|
| 36 |
+
must review and accept that agreement when installing or using the dependency.
|
| 37 |
+
|
| 38 |
+
NVIDIA, CUDA, and Blackwell are trademarks and do not imply endorsement.
|
| 39 |
+
|
benchmark_results.json
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 1,
|
| 3 |
+
"date": "2026-07-12",
|
| 4 |
+
"hardware": {
|
| 5 |
+
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
| 6 |
+
"compute_capability": "SM120",
|
| 7 |
+
"user_batch_size": 1
|
| 8 |
+
},
|
| 9 |
+
"software": {
|
| 10 |
+
"python": "3.12",
|
| 11 |
+
"torch": "2.7.1+cu128",
|
| 12 |
+
"torch_cuda": "12.8",
|
| 13 |
+
"flashinfer": "0.6.14",
|
| 14 |
+
"nvidia_cutlass_dsl": "4.5.2"
|
| 15 |
+
},
|
| 16 |
+
"natural_eight_text_suite": {
|
| 17 |
+
"definition": "Pipeline and T3 seconds are sums of per-case medians; aggregate RTF is summed pipeline time divided by summed generated-audio duration; T3 throughput uses valid speech tokens; memory is maximum per-case median PyTorch CUDA peak.",
|
| 18 |
+
"d16_quality": {
|
| 19 |
+
"bf16": {
|
| 20 |
+
"audio_seconds": 98.64,
|
| 21 |
+
"pipeline_seconds": 9.304738,
|
| 22 |
+
"aggregate_rtf": 0.0943303,
|
| 23 |
+
"t3_seconds": 7.512483,
|
| 24 |
+
"valid_tokens": 2466,
|
| 25 |
+
"t3_tokens_per_second": 328.254,
|
| 26 |
+
"peak_allocated_bytes": 6423756800,
|
| 27 |
+
"peak_reserved_bytes": 7107248128
|
| 28 |
+
},
|
| 29 |
+
"nvfp4_w4a16": {
|
| 30 |
+
"audio_seconds": 92.52,
|
| 31 |
+
"pipeline_seconds": 8.072466,
|
| 32 |
+
"aggregate_rtf": 0.087251,
|
| 33 |
+
"t3_seconds": 6.330483,
|
| 34 |
+
"valid_tokens": 2313,
|
| 35 |
+
"t3_tokens_per_second": 365.375,
|
| 36 |
+
"peak_allocated_bytes": 5214659584,
|
| 37 |
+
"peak_reserved_bytes": 6322913280
|
| 38 |
+
},
|
| 39 |
+
"nvfp4_vs_bf16": {
|
| 40 |
+
"aggregate_rtf_speedup": 1.081136,
|
| 41 |
+
"t3_throughput_speedup": 1.113087,
|
| 42 |
+
"peak_allocated_reduction_bytes": 1209097216,
|
| 43 |
+
"peak_allocated_reduction_percent": 18.8223,
|
| 44 |
+
"peak_reserved_reduction_bytes": 784334848,
|
| 45 |
+
"peak_reserved_reduction_percent": 11.0357
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"d24_fast": {
|
| 49 |
+
"bf16": {
|
| 50 |
+
"audio_seconds": 93.36,
|
| 51 |
+
"pipeline_seconds": 6.797084,
|
| 52 |
+
"aggregate_rtf": 0.0728051,
|
| 53 |
+
"t3_seconds": 5.04703,
|
| 54 |
+
"valid_tokens": 2334,
|
| 55 |
+
"t3_tokens_per_second": 462.45,
|
| 56 |
+
"peak_allocated_bytes": 6539530752,
|
| 57 |
+
"peak_reserved_bytes": 7411335168
|
| 58 |
+
},
|
| 59 |
+
"nvfp4_w4a16": {
|
| 60 |
+
"audio_seconds": 91.8,
|
| 61 |
+
"pipeline_seconds": 6.483264,
|
| 62 |
+
"aggregate_rtf": 0.0706238,
|
| 63 |
+
"t3_seconds": 4.748782,
|
| 64 |
+
"valid_tokens": 2295,
|
| 65 |
+
"t3_tokens_per_second": 483.282,
|
| 66 |
+
"peak_allocated_bytes": 5129031168,
|
| 67 |
+
"peak_reserved_bytes": 6064963584
|
| 68 |
+
},
|
| 69 |
+
"nvfp4_vs_bf16": {
|
| 70 |
+
"aggregate_rtf_speedup": 1.030886,
|
| 71 |
+
"t3_throughput_speedup": 1.045046,
|
| 72 |
+
"peak_allocated_reduction_bytes": 1410499584,
|
| 73 |
+
"peak_allocated_reduction_percent": 21.5688,
|
| 74 |
+
"peak_reserved_reduction_bytes": 1346371584,
|
| 75 |
+
"peak_reserved_reduction_percent": 18.1664
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
"fixed_capped_work": {
|
| 80 |
+
"definition": "One fixed-seed case, five-repeat medians, 1024-token reservation. D16 caps at 32 speech tokens; D24 caps at 48, so compare quantized and BF16 only within each block size.",
|
| 81 |
+
"d16": {
|
| 82 |
+
"bf16_t3_ms": 88.363,
|
| 83 |
+
"nvfp4_t3_ms": 78.475,
|
| 84 |
+
"t3_speedup": 1.125998,
|
| 85 |
+
"bf16_pipeline_ms": 162.897,
|
| 86 |
+
"nvfp4_pipeline_ms": 153.235,
|
| 87 |
+
"pipeline_speedup": 1.063052,
|
| 88 |
+
"bf16_peak_allocated_bytes": 4165647360,
|
| 89 |
+
"nvfp4_peak_allocated_bytes": 2745680896,
|
| 90 |
+
"peak_allocated_reduction_percent": 34.0875,
|
| 91 |
+
"peak_reserved_reduction_percent": 30.0444
|
| 92 |
+
},
|
| 93 |
+
"d24": {
|
| 94 |
+
"bf16_t3_ms": 91.192,
|
| 95 |
+
"nvfp4_t3_ms": 85.574,
|
| 96 |
+
"t3_speedup": 1.065645,
|
| 97 |
+
"bf16_pipeline_ms": 180.938,
|
| 98 |
+
"nvfp4_pipeline_ms": 176.006,
|
| 99 |
+
"pipeline_speedup": 1.028023,
|
| 100 |
+
"bf16_peak_allocated_bytes": 4173167104,
|
| 101 |
+
"nvfp4_peak_allocated_bytes": 2754244096,
|
| 102 |
+
"peak_allocated_reduction_percent": 34.0011,
|
| 103 |
+
"peak_reserved_reduction_percent": 30.0689
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
"automated_quality": {
|
| 107 |
+
"warning": "Small eight-text, one-reference automated evaluation; not human MOS or a multi-speaker benchmark.",
|
| 108 |
+
"d16": {
|
| 109 |
+
"bf16_mean_score": 94.125,
|
| 110 |
+
"nvfp4_mean_score": 94.625,
|
| 111 |
+
"exact_transcripts_each": "8/8",
|
| 112 |
+
"hard_failures_each": "0/8",
|
| 113 |
+
"order_controlled_pairwise": "7 ties, 1 BF16 win"
|
| 114 |
+
},
|
| 115 |
+
"d24": {
|
| 116 |
+
"bf16_mean_score": 95.0,
|
| 117 |
+
"nvfp4_mean_score": 93.125,
|
| 118 |
+
"exact_transcripts_each": "8/8",
|
| 119 |
+
"hard_failures_each": "0/8",
|
| 120 |
+
"order_controlled_pairwise": "6 ties, 1 BF16 win, 1 NVFP4 win"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"source_free_load_diagnostic": {
|
| 124 |
+
"fresh_process_load_seconds_with_base_hash_verification_and_jit_cache_hit": 2.675073,
|
| 125 |
+
"allocated_bytes_after_load": 1444340224,
|
| 126 |
+
"reserved_bytes_after_load": 1478492160,
|
| 127 |
+
"prepared_projection_bytes": 283116000,
|
| 128 |
+
"prepared_projection_count": 120,
|
| 129 |
+
"short_generation_peak_allocated_bytes": 2548431360,
|
| 130 |
+
"first_same_process_generation_seconds_with_exact_shape_config_cache": 10.567298,
|
| 131 |
+
"second_same_process_generation_seconds": 0.153511,
|
| 132 |
+
"first_to_warm_ratio": 68.8376,
|
| 133 |
+
"notes": "Canonical weights were prepared without BF16 projection materialization or nvfp4_quantize. The first-generation measurement includes engine creation, CUDA graph capture, and per-process FlashInfer setup even though the exact shape's selected config was cached. A completely missing exact-shape tuning entry can take longer. Caches are GPU-specific and are intentionally not distributed."
|
| 134 |
+
},
|
| 135 |
+
"interpretation_warnings": [
|
| 136 |
+
"Do not present raw natural-suite wall-time ratios as pure NVFP4 speedups because BF16 and NVFP4 sampled different token counts.",
|
| 137 |
+
"PyTorch allocated/reserved peaks are not total board VRAM or nvidia-smi process usage.",
|
| 138 |
+
"The D24 versus D16 speed difference is primarily the block-size change, not quantization alone.",
|
| 139 |
+
"The D16 BF16 natural control had one token-cap hit and one duration warning; NVFP4 did not.",
|
| 140 |
+
"D32 was rejected for release use after cap hits and an OOM in the natural suite."
|
| 141 |
+
]
|
| 142 |
+
}
|
example.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate one zero-shot voice-cloned utterance with the local NVFP4 bundle."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import re
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import soundfile as sf
|
| 11 |
+
import torch
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
|
| 14 |
+
from chatterbox_flash.nvfp4 import (
|
| 15 |
+
BASE_ASSET_FILENAMES,
|
| 16 |
+
BASE_CHECKPOINT_REVISION,
|
| 17 |
+
BASE_REPO_ID,
|
| 18 |
+
load_prepacked_tts_local,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_args() -> argparse.Namespace:
|
| 23 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 24 |
+
parser.add_argument("--prompt-wav", type=Path, required=True)
|
| 25 |
+
parser.add_argument("--text", required=True)
|
| 26 |
+
parser.add_argument("--output", type=Path, default=Path("output.wav"))
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--block-size",
|
| 29 |
+
type=int,
|
| 30 |
+
choices=(16, 24),
|
| 31 |
+
default=16,
|
| 32 |
+
help="16 is the quality profile; 24 is the optional faster profile",
|
| 33 |
+
)
|
| 34 |
+
parser.add_argument("--seed", type=int, default=4242)
|
| 35 |
+
return parser.parse_args()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def main() -> None:
|
| 39 |
+
args = parse_args()
|
| 40 |
+
if not args.prompt_wav.is_file():
|
| 41 |
+
raise SystemExit(f"Missing reference audio: {args.prompt_wav}")
|
| 42 |
+
|
| 43 |
+
bundle = Path(__file__).resolve().parent
|
| 44 |
+
base_dir = Path(snapshot_download(
|
| 45 |
+
repo_id=BASE_REPO_ID,
|
| 46 |
+
revision=BASE_CHECKPOINT_REVISION,
|
| 47 |
+
allow_patterns=list(BASE_ASSET_FILENAMES),
|
| 48 |
+
))
|
| 49 |
+
capability = torch.cuda.get_device_capability()
|
| 50 |
+
gpu_slug = re.sub(r"[^a-zA-Z0-9_.-]+", "-", torch.cuda.get_device_name())
|
| 51 |
+
autotune_cache = (
|
| 52 |
+
Path.home()
|
| 53 |
+
/ ".cache"
|
| 54 |
+
/ "chatterbox-flash-nvfp4"
|
| 55 |
+
/ f"flashinfer-{gpu_slug}-sm{capability[0]}{capability[1]}-d{args.block_size}.json"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
torch.manual_seed(args.seed)
|
| 59 |
+
torch.cuda.manual_seed_all(args.seed)
|
| 60 |
+
tts = load_prepacked_tts_local(
|
| 61 |
+
bundle,
|
| 62 |
+
base_dir,
|
| 63 |
+
device="cuda",
|
| 64 |
+
drf_block_size=args.block_size,
|
| 65 |
+
autotune_cache=autotune_cache,
|
| 66 |
+
)
|
| 67 |
+
waveform = tts.generate(
|
| 68 |
+
args.text,
|
| 69 |
+
audio_prompt_path=args.prompt_wav,
|
| 70 |
+
exaggeration=0.5,
|
| 71 |
+
num_steps=10,
|
| 72 |
+
temperature=0.2,
|
| 73 |
+
time_shift_tau=0.5,
|
| 74 |
+
omnivoice_schedule_t_shift=0.5,
|
| 75 |
+
cfg_scale=1.0,
|
| 76 |
+
position_temperature=5.0,
|
| 77 |
+
n_cfm_timesteps=2,
|
| 78 |
+
backend="flashinfer",
|
| 79 |
+
use_cuda_graph=True,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
sf.write(args.output, waveform.float().numpy(), tts.sr)
|
| 84 |
+
print(f"Wrote {args.output} at {tts.sr} Hz")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
main()
|
| 89 |
+
|
nvfp4_config.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runtime/chatterbox-flash/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.egg-info/
|
| 5 |
+
build/
|
| 6 |
+
dist/
|
| 7 |
+
.eggs/
|
| 8 |
+
|
| 9 |
+
# Virtual environments (uv venv, etc.)
|
| 10 |
+
.venv/
|
| 11 |
+
.venv*/
|
| 12 |
+
venv/
|
| 13 |
+
env/
|
| 14 |
+
|
| 15 |
+
# Model checkpoints — released via the Hugging Face Hub, too large for git
|
| 16 |
+
checkpoints/
|
| 17 |
+
|
| 18 |
+
# Generated audio / test outputs
|
| 19 |
+
/test_out/
|
| 20 |
+
/syn_out/
|
| 21 |
+
/out.wav
|
| 22 |
+
/*.wav
|
| 23 |
+
eval_out_smoke/
|
| 24 |
+
|
| 25 |
+
# Internal-only scripts (not part of the released package)
|
| 26 |
+
smoke_test.py
|
| 27 |
+
upload_hf.py
|
| 28 |
+
scripts/convert_checkpoint.py
|
| 29 |
+
# Demo-page sites are separate git repos (own GitHub remotes), not part of this package
|
| 30 |
+
*_demopage/
|
runtime/chatterbox-flash/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Resemble AI
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
runtime/chatterbox-flash/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chatterbox-Flash NVFP4 runtime
|
| 2 |
+
|
| 3 |
+
This is the installable runtime shipped with the unofficial
|
| 4 |
+
Chatterbox-Flash T3 W4A16 NVFP4 model bundle. It vendors the MIT-licensed
|
| 5 |
+
Chatterbox-Flash source at commit
|
| 6 |
+
`74e05baa8ce574bf2cc571702391a21f1b0d48c5` and adds an engine-local,
|
| 7 |
+
source-free NVFP4 projection provider plus canonical checkpoint loader.
|
| 8 |
+
|
| 9 |
+
Install only through the bundle's locked `uv sync --extra flashinfer` command.
|
| 10 |
+
See the model card at the bundle root for hardware requirements, usage,
|
| 11 |
+
benchmarks, attribution, and limitations.
|
runtime/chatterbox-flash/chatterbox_flash/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chatterbox-Flash — prior-calibrated block-diffusion zero-shot TTS.
|
| 2 |
+
|
| 3 |
+
Public API:
|
| 4 |
+
ChatterboxFlashTTS — end-to-end TTS pipeline (T3 + S3Gen + VE + tokenizer)
|
| 5 |
+
ChatterboxFlashT3 — block-diffusion T3 decoder (low-level)
|
| 6 |
+
Conditionals — cacheable speaker / prompt conditioning bundle
|
| 7 |
+
en_us_cleaner — English text normalization used at training / eval time
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from .engines import FLASHINFER_AVAILABLE, MLX_AVAILABLE, build_engine
|
| 11 |
+
from .model import ChatterboxFlashT3
|
| 12 |
+
from .nvfp4 import (
|
| 13 |
+
CanonicalNVFP4ProjectionProvider,
|
| 14 |
+
load_prepacked_t3,
|
| 15 |
+
load_prepacked_tts_from_hub,
|
| 16 |
+
load_prepacked_tts_local,
|
| 17 |
+
)
|
| 18 |
+
from .text_norm import en_us_cleaner
|
| 19 |
+
from .tts import ChatterboxFlashTTS, Conditionals
|
| 20 |
+
|
| 21 |
+
__version__ = "0.1.0+nvfp4.1"
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
"ChatterboxFlashT3",
|
| 25 |
+
"ChatterboxFlashTTS",
|
| 26 |
+
"CanonicalNVFP4ProjectionProvider",
|
| 27 |
+
"Conditionals",
|
| 28 |
+
"FLASHINFER_AVAILABLE",
|
| 29 |
+
"MLX_AVAILABLE",
|
| 30 |
+
"build_engine",
|
| 31 |
+
"en_us_cleaner",
|
| 32 |
+
"load_prepacked_t3",
|
| 33 |
+
"load_prepacked_tts_from_hub",
|
| 34 |
+
"load_prepacked_tts_local",
|
| 35 |
+
"__version__",
|
| 36 |
+
]
|
runtime/chatterbox-flash/chatterbox_flash/attention_flashinfer.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backwards-compatibility shim — :class:`FlashInferEngine` now lives in
|
| 2 |
+
:mod:`chatterbox_flash.engines.flashinfer`.
|
| 3 |
+
|
| 4 |
+
The original module path is preserved so existing scripts that did
|
| 5 |
+
|
| 6 |
+
from chatterbox_flash.attention_flashinfer import FlashInferEngine
|
| 7 |
+
|
| 8 |
+
continue to work. New code should import from
|
| 9 |
+
``chatterbox_flash.engines`` (or call :func:`chatterbox_flash.engines.build_engine`)
|
| 10 |
+
instead.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from .engines.flashinfer import ( # noqa: F401
|
| 16 |
+
FlashInferEngine,
|
| 17 |
+
flashinfer_available,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
__all__ = ["FlashInferEngine", "flashinfer_available"]
|
runtime/chatterbox-flash/chatterbox_flash/calibration.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prior-calibrated PMI scoring for Chatterbox-Flash block decoding.
|
| 2 |
+
|
| 3 |
+
The decoder ranks masked positions by a self-calibrated pointwise mutual
|
| 4 |
+
information score that suppresses the dominant-token bias of discrete speech
|
| 5 |
+
codecs (no per-token silence IDs needed):
|
| 6 |
+
|
| 7 |
+
s_i = log p_i(x_i) - lambda * log p_bar(x_i)
|
| 8 |
+
|
| 9 |
+
where ``p_bar`` is a reference marginal — either the precomputed unconditional
|
| 10 |
+
block prior (see :meth:`ChatterboxFlashT3._compute_uncond_block_prior`) or the
|
| 11 |
+
step-0 block-mean ``mean_j p_j``.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from torch import Tensor
|
| 18 |
+
|
| 19 |
+
_LOG_EPS = 1e-8
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _marginal_for_sampled(
|
| 23 |
+
probs_bl: Tensor,
|
| 24 |
+
sampled: Tensor,
|
| 25 |
+
marginal_prior: Tensor | None,
|
| 26 |
+
) -> Tensor:
|
| 27 |
+
"""Return per-position marginal probability for each sampled token.
|
| 28 |
+
|
| 29 |
+
If ``marginal_prior`` is given (shape ``(V,)``), index into it directly.
|
| 30 |
+
Otherwise compute the block-mean marginal on the fly.
|
| 31 |
+
"""
|
| 32 |
+
if marginal_prior is not None:
|
| 33 |
+
return marginal_prior[sampled]
|
| 34 |
+
bl = probs_bl.size(0)
|
| 35 |
+
sampled_cols = sampled.unsqueeze(0).expand(bl, -1)
|
| 36 |
+
return probs_bl.gather(1, sampled_cols).mean(dim=0)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def self_calibrated_pmi(
|
| 40 |
+
probs_bl: Tensor,
|
| 41 |
+
sampled: Tensor,
|
| 42 |
+
eps: float = _LOG_EPS,
|
| 43 |
+
marginal_prior: Tensor | None = None,
|
| 44 |
+
prior_lambda: float = 1.0,
|
| 45 |
+
) -> Tensor:
|
| 46 |
+
"""
|
| 47 |
+
Per-position PMI-style score vs a marginal prior.
|
| 48 |
+
|
| 49 |
+
s_i = log p_i(x_i) - lambda * log p_bar(x_i),
|
| 50 |
+
p_bar(v) = mean_j p_j(v).
|
| 51 |
+
|
| 52 |
+
When ``marginal_prior`` (shape ``(V,)``) is provided, it is used as
|
| 53 |
+
``p_bar`` instead of the current block-mean — typically the precomputed
|
| 54 |
+
unconditional block prior for a consistent, uninformed reference.
|
| 55 |
+
"""
|
| 56 |
+
pi = probs_bl.gather(1, sampled.unsqueeze(1)).squeeze(1)
|
| 57 |
+
p_bar_sampled = _marginal_for_sampled(probs_bl, sampled, marginal_prior)
|
| 58 |
+
return torch.log(pi.clamp_min(eps)) - (
|
| 59 |
+
float(prior_lambda) * torch.log(p_bar_sampled.clamp_min(eps))
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def compute_pmi(
|
| 64 |
+
probs_bl: Tensor,
|
| 65 |
+
sampled: Tensor,
|
| 66 |
+
*,
|
| 67 |
+
marginal_prior: Tensor | None = None,
|
| 68 |
+
prior_lambda: float = 1.0,
|
| 69 |
+
eps: float = _LOG_EPS,
|
| 70 |
+
) -> Tensor:
|
| 71 |
+
"""Compute self-calibrated PMI.
|
| 72 |
+
|
| 73 |
+
When ``marginal_prior`` (shape ``(V,)``) is given, it is used as the
|
| 74 |
+
denominator instead of computing ``probs_bl.mean(dim=0)`` on the fly.
|
| 75 |
+
"""
|
| 76 |
+
return self_calibrated_pmi(
|
| 77 |
+
probs_bl,
|
| 78 |
+
sampled,
|
| 79 |
+
eps=eps,
|
| 80 |
+
marginal_prior=marginal_prior,
|
| 81 |
+
prior_lambda=prior_lambda,
|
| 82 |
+
)
|
runtime/chatterbox-flash/chatterbox_flash/cfg_guidance.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Classifier-free guidance helpers for Chatterbox-Flash.
|
| 2 |
+
|
| 3 |
+
We only keep the ``zero_text_batch`` recipe used in the paper: at each block
|
| 4 |
+
step the forward batch is doubled to ``2 * B``; rows ``0..B-1`` carry the full
|
| 5 |
+
text + conditioning, rows ``B..2B-1`` carry a null branch where the text and
|
| 6 |
+
the entire conditioning prefix are zeroed (``cfg_null_cond_mode="zero_all"``).
|
| 7 |
+
The two halves of the logits are then combined via
|
| 8 |
+
``log p' = log p_c + w * (log p_c - log p_u)``.
|
| 9 |
+
|
| 10 |
+
The same combination is applied on the **PMI scale** as well (see
|
| 11 |
+
:func:`pmi_cfg_combine`) — this is the only PMI/CFG combination supported;
|
| 12 |
+
no alternative ``cfg_prior_mode`` is exposed.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from torch import Tensor
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def omnivoice_cfg_log_probs(
|
| 23 |
+
c_log_probs: Tensor,
|
| 24 |
+
u_log_probs: Tensor,
|
| 25 |
+
guidance_scale: float,
|
| 26 |
+
) -> Tensor:
|
| 27 |
+
"""``log p' = log p_c + w * (log p_c - log p_u)`` (elementwise)."""
|
| 28 |
+
if guidance_scale == 0.0:
|
| 29 |
+
return c_log_probs
|
| 30 |
+
return c_log_probs + guidance_scale * (c_log_probs - u_log_probs)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def apply_zero_text_cfg_from_logits(
|
| 34 |
+
logits_cond: Tensor,
|
| 35 |
+
logits_u: Tensor,
|
| 36 |
+
guidance_scale: float,
|
| 37 |
+
) -> Tensor:
|
| 38 |
+
"""CFG combination from two logit tensors of identical shape.
|
| 39 |
+
|
| 40 |
+
Operates on log-softmax so the result is a valid scoring/sampling logit
|
| 41 |
+
(softmax recovers the guided distribution).
|
| 42 |
+
"""
|
| 43 |
+
if guidance_scale == 0.0:
|
| 44 |
+
return logits_cond
|
| 45 |
+
c_log = F.log_softmax(logits_cond, dim=-1)
|
| 46 |
+
u_log = F.log_softmax(logits_u, dim=-1)
|
| 47 |
+
return omnivoice_cfg_log_probs(c_log, u_log, guidance_scale)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def pmi_cfg_combine(
|
| 51 |
+
pmi_c: Tensor,
|
| 52 |
+
pmi_u: Tensor,
|
| 53 |
+
guidance_scale: float,
|
| 54 |
+
) -> Tensor:
|
| 55 |
+
"""Guidance-combine PMI scores: ``pmi = (1+w) * pmi_c - w * pmi_u``."""
|
| 56 |
+
w = float(guidance_scale)
|
| 57 |
+
return (1.0 + w) * pmi_c - w * pmi_u
|
runtime/chatterbox-flash/chatterbox_flash/engines/__init__.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference engines for Chatterbox-Flash.
|
| 2 |
+
|
| 3 |
+
Three backends are provided, all implementing the :class:`InferenceEngine`
|
| 4 |
+
protocol consumed by :class:`~chatterbox_flash.model.ChatterboxFlashT3`:
|
| 5 |
+
|
| 6 |
+
* :class:`FlashInferEngine` — paged KV cache + custom attention kernels
|
| 7 |
+
via the ``flashinfer-python`` package; supports CUDA-graph capture of
|
| 8 |
+
the block forward. This is what the paper numbers were produced with
|
| 9 |
+
and what should be used for any throughput-sensitive workload on
|
| 10 |
+
CUDA.
|
| 11 |
+
|
| 12 |
+
* :class:`TorchSDPAEngine` — pure PyTorch fallback using the HuggingFace
|
| 13 |
+
transformers SDPA backend with a :class:`DynamicCache`. No extra
|
| 14 |
+
dependencies, runs on any CUDA / CPU / MPS device the underlying T3
|
| 15 |
+
supports. No CUDA graphs, slower per step, but produces numerically
|
| 16 |
+
matching output (modulo small fp32/bf16 reduction order differences).
|
| 17 |
+
|
| 18 |
+
* :class:`MLXEngine` *(experimental)* — Apple-Silicon native Metal
|
| 19 |
+
backend. Wraps the mlx-lm LLaMA backbone and bypasses its
|
| 20 |
+
``embed_tokens`` layer so the chatterbox-flash custom embeddings can
|
| 21 |
+
be used unchanged. Requires ``mlx`` and ``mlx-lm`` installed.
|
| 22 |
+
|
| 23 |
+
Use :func:`build_engine` to instantiate; it auto-falls-back to SDPA when
|
| 24 |
+
FlashInfer is unavailable. MLX must be selected explicitly via
|
| 25 |
+
``backend="mlx"`` (or the ``CHATTERBOX_FLASH_ENGINE=mlx`` env var).
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import importlib.util
|
| 31 |
+
import logging
|
| 32 |
+
import os
|
| 33 |
+
from typing import Literal
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
|
| 37 |
+
from .base import InferenceEngine
|
| 38 |
+
from .torch_sdpa import TorchSDPAEngine
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger(__name__)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
FLASHINFER_AVAILABLE: bool = importlib.util.find_spec("flashinfer") is not None
|
| 44 |
+
MLX_AVAILABLE: bool = (
|
| 45 |
+
importlib.util.find_spec("mlx") is not None
|
| 46 |
+
and importlib.util.find_spec("mlx_lm") is not None
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
_Backend = Literal["auto", "flashinfer", "torch", "mlx"]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _resolve_backend(requested: _Backend) -> Literal["flashinfer", "torch", "mlx"]:
|
| 54 |
+
"""Pick a backend honoring ``CHATTERBOX_FLASH_ENGINE`` env override."""
|
| 55 |
+
env = os.environ.get("CHATTERBOX_FLASH_ENGINE", "").strip().lower()
|
| 56 |
+
if env in ("flashinfer", "torch", "sdpa", "mlx"):
|
| 57 |
+
requested = "torch" if env == "sdpa" else env # type: ignore[assignment]
|
| 58 |
+
|
| 59 |
+
if requested == "auto":
|
| 60 |
+
return "flashinfer" if FLASHINFER_AVAILABLE else "torch"
|
| 61 |
+
if requested == "flashinfer":
|
| 62 |
+
if not FLASHINFER_AVAILABLE:
|
| 63 |
+
raise ImportError(
|
| 64 |
+
"flashinfer-python is not installed. Install with "
|
| 65 |
+
"`pip install flashinfer-python` or pass backend='torch'.",
|
| 66 |
+
)
|
| 67 |
+
return "flashinfer"
|
| 68 |
+
if requested == "mlx":
|
| 69 |
+
if not MLX_AVAILABLE:
|
| 70 |
+
raise ImportError(
|
| 71 |
+
"MLX backend requires the 'mlx' and 'mlx-lm' packages. "
|
| 72 |
+
"Install with `pip install mlx mlx-lm` "
|
| 73 |
+
"(Apple Silicon Mac only).",
|
| 74 |
+
)
|
| 75 |
+
return "mlx"
|
| 76 |
+
if requested in ("torch", "sdpa"):
|
| 77 |
+
return "torch"
|
| 78 |
+
raise ValueError(
|
| 79 |
+
f"backend must be one of 'auto', 'flashinfer', 'torch', 'mlx'; "
|
| 80 |
+
f"got {requested!r}",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def build_engine(
|
| 85 |
+
model: torch.nn.Module,
|
| 86 |
+
max_seq_len: int,
|
| 87 |
+
dtype: torch.dtype,
|
| 88 |
+
*,
|
| 89 |
+
backend: _Backend = "auto",
|
| 90 |
+
batch_size: int = 1,
|
| 91 |
+
page_size: int = 16,
|
| 92 |
+
) -> InferenceEngine:
|
| 93 |
+
"""Construct an inference engine.
|
| 94 |
+
|
| 95 |
+
Parameters
|
| 96 |
+
----------
|
| 97 |
+
model: the :class:`ChatterboxFlashT3` instance.
|
| 98 |
+
max_seq_len: upper bound on ``prefix_len + total_speech_len`` for the
|
| 99 |
+
cache reservation; needed by the FlashInfer paged buffer. The
|
| 100 |
+
torch SDPA / MLX engines use this only as a hint.
|
| 101 |
+
dtype: model parameter dtype (bf16 / fp16 / fp32).
|
| 102 |
+
backend: ``"auto"`` (default, prefer FlashInfer) / ``"flashinfer"`` /
|
| 103 |
+
``"torch"`` / ``"mlx"``. Environment variable
|
| 104 |
+
``CHATTERBOX_FLASH_ENGINE`` overrides this.
|
| 105 |
+
batch_size: number of forward-batch rows (with zero-text-batch CFG
|
| 106 |
+
this is ``2 × B_usr``).
|
| 107 |
+
page_size: FlashInfer paged-cache page size (ignored elsewhere).
|
| 108 |
+
"""
|
| 109 |
+
chosen = _resolve_backend(backend)
|
| 110 |
+
projection_provider = getattr(model, "_flashinfer_projection_provider", None)
|
| 111 |
+
if (
|
| 112 |
+
projection_provider is not None
|
| 113 |
+
and bool(getattr(projection_provider, "source_free", False))
|
| 114 |
+
and chosen != "flashinfer"
|
| 115 |
+
):
|
| 116 |
+
raise RuntimeError(
|
| 117 |
+
"This model has no torch/MLX transformer projection weights. "
|
| 118 |
+
"Its source-free NVFP4 projection provider requires "
|
| 119 |
+
"backend='flashinfer'."
|
| 120 |
+
)
|
| 121 |
+
if chosen == "flashinfer":
|
| 122 |
+
from .flashinfer import FlashInferEngine
|
| 123 |
+
logger.debug(
|
| 124 |
+
"build_engine: backend=flashinfer max_seq=%d dtype=%s batch=%d page=%d",
|
| 125 |
+
max_seq_len, dtype, batch_size, page_size,
|
| 126 |
+
)
|
| 127 |
+
return FlashInferEngine(
|
| 128 |
+
model, max_seq_len, dtype,
|
| 129 |
+
batch_size=batch_size, page_size=page_size,
|
| 130 |
+
)
|
| 131 |
+
if chosen == "mlx":
|
| 132 |
+
from .mlx import MLXEngine
|
| 133 |
+
logger.debug(
|
| 134 |
+
"build_engine: backend=mlx max_seq=%d dtype=%s batch=%d",
|
| 135 |
+
max_seq_len, dtype, batch_size,
|
| 136 |
+
)
|
| 137 |
+
return MLXEngine(
|
| 138 |
+
model, max_seq_len, dtype, batch_size=batch_size,
|
| 139 |
+
)
|
| 140 |
+
logger.debug(
|
| 141 |
+
"build_engine: backend=torch (SDPA fallback) max_seq=%d dtype=%s batch=%d",
|
| 142 |
+
max_seq_len, dtype, batch_size,
|
| 143 |
+
)
|
| 144 |
+
return TorchSDPAEngine(
|
| 145 |
+
model, max_seq_len, dtype, batch_size=batch_size,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def __getattr__(name: str): # noqa: D401
|
| 150 |
+
"""Lazy attribute access — only import optional backends if asked for."""
|
| 151 |
+
if name == "FlashInferEngine":
|
| 152 |
+
from .flashinfer import FlashInferEngine
|
| 153 |
+
return FlashInferEngine
|
| 154 |
+
if name == "MLXEngine":
|
| 155 |
+
from .mlx import MLXEngine
|
| 156 |
+
return MLXEngine
|
| 157 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
__all__ = [
|
| 161 |
+
"InferenceEngine",
|
| 162 |
+
"TorchSDPAEngine",
|
| 163 |
+
"FlashInferEngine",
|
| 164 |
+
"MLXEngine",
|
| 165 |
+
"FLASHINFER_AVAILABLE",
|
| 166 |
+
"MLX_AVAILABLE",
|
| 167 |
+
"build_engine",
|
| 168 |
+
]
|
runtime/chatterbox-flash/chatterbox_flash/engines/base.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Abstract inference-engine protocol shared by the FlashInfer and the
|
| 2 |
+
torch-SDPA backends.
|
| 3 |
+
|
| 4 |
+
The block-diffusion ``generate()`` loop is engine-agnostic: it only ever
|
| 5 |
+
talks to the engine through these six methods. Concrete backends differ
|
| 6 |
+
in how they store the KV cache (paged buffer vs. HuggingFace
|
| 7 |
+
:class:`~transformers.DynamicCache`), how they batch the prefix
|
| 8 |
+
forward, and whether they can capture the per-block forward as a CUDA
|
| 9 |
+
graph.
|
| 10 |
+
|
| 11 |
+
Contract
|
| 12 |
+
--------
|
| 13 |
+
The engine manages a single KV cache shared by the conditioning prefix
|
| 14 |
+
and the speech-block forwards. Lifecycle for one ``generate()`` call::
|
| 15 |
+
|
| 16 |
+
engine.reset()
|
| 17 |
+
shift_ctx = engine.prefix_forward(prefix_emb_or_list) # (B, 1, dim)
|
| 18 |
+
engine.capture_cuda_graph(BS, speech_head=speech_head) # optional
|
| 19 |
+
|
| 20 |
+
for b in range(num_blocks):
|
| 21 |
+
for k in range(K):
|
| 22 |
+
engine.set_shift_ctx(shift_ctx) # graph engines only
|
| 23 |
+
hidden, logits = engine.block_forward_graph(block_emb, cs)
|
| 24 |
+
# or: hidden = engine.block_forward(block_emb, cs)
|
| 25 |
+
|
| 26 |
+
engine.advance_cache(block_len)
|
| 27 |
+
# caller commits the block's clean tokens via block_forward(...)
|
| 28 |
+
# before advance_cache; shift_ctx is updated from that hidden.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
from typing import List, Optional, Protocol, Tuple, Union, runtime_checkable
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
from torch import Tensor
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
CacheStarts = Union[int, List[int]]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@runtime_checkable
|
| 43 |
+
class InferenceEngine(Protocol):
|
| 44 |
+
"""Minimal protocol implemented by all Chatterbox-Flash engines."""
|
| 45 |
+
|
| 46 |
+
has_cuda_graph: bool
|
| 47 |
+
"""``True`` iff :meth:`capture_cuda_graph` was called and succeeded."""
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def cache_len(self) -> int:
|
| 51 |
+
"""Current per-row KV cache occupancy (uniform across the batch)."""
|
| 52 |
+
...
|
| 53 |
+
|
| 54 |
+
def reset(self) -> None:
|
| 55 |
+
"""Clear cache contents and free CUDA-graph state for the next call."""
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
def can_reuse(
|
| 59 |
+
self,
|
| 60 |
+
max_seq_len: int,
|
| 61 |
+
dtype: torch.dtype,
|
| 62 |
+
*,
|
| 63 |
+
batch_size: int,
|
| 64 |
+
page_size: int = 16,
|
| 65 |
+
) -> bool:
|
| 66 |
+
"""Whether this engine can be re-used for a request with these shape/dtype."""
|
| 67 |
+
...
|
| 68 |
+
|
| 69 |
+
# --- prefix / block forwards -------------------------------------- #
|
| 70 |
+
|
| 71 |
+
def prefix_forward(
|
| 72 |
+
self,
|
| 73 |
+
prefix_emb_or_list: Union[Tensor, List[Tensor]],
|
| 74 |
+
) -> Tensor:
|
| 75 |
+
"""Encode the conditioning prefix once.
|
| 76 |
+
|
| 77 |
+
``prefix_emb_or_list`` is either a stacked tensor ``(B_fwd, lp, dim)``
|
| 78 |
+
(same-text path) or a list of length ``B_fwd`` of per-row prefixes
|
| 79 |
+
with different lengths (cross-text path). Writes the prefix KV
|
| 80 |
+
into the engine's cache and returns ``shift_ctx`` — the hidden
|
| 81 |
+
state of the SOS row, shape ``(B_fwd, 1, dim)``.
|
| 82 |
+
"""
|
| 83 |
+
...
|
| 84 |
+
|
| 85 |
+
def block_forward(
|
| 86 |
+
self,
|
| 87 |
+
block_emb: Tensor,
|
| 88 |
+
cache_starts: CacheStarts,
|
| 89 |
+
) -> Tensor:
|
| 90 |
+
"""Eager (non-CUDA-graph) block forward.
|
| 91 |
+
|
| 92 |
+
``block_emb`` shape: ``(B_fwd, bl, dim)``.
|
| 93 |
+
``cache_starts`` is either an int (uniform start position) or a
|
| 94 |
+
per-row list (cross-text / compact-null-prefix path).
|
| 95 |
+
Returns hidden state ``(B_fwd, bl, dim)``; logits are computed by
|
| 96 |
+
the caller via ``speech_head``.
|
| 97 |
+
"""
|
| 98 |
+
...
|
| 99 |
+
|
| 100 |
+
def block_forward_graph(
|
| 101 |
+
self,
|
| 102 |
+
block_emb: Tensor,
|
| 103 |
+
cache_starts: CacheStarts,
|
| 104 |
+
) -> Tuple[Tensor, Optional[Tensor]]:
|
| 105 |
+
"""CUDA-graph replay of the block forward.
|
| 106 |
+
|
| 107 |
+
Engines that don't support CUDA graphs (e.g. torch SDPA) should
|
| 108 |
+
return ``(self.block_forward(block_emb, cache_starts), None)`` so
|
| 109 |
+
the caller can fall back uniformly. When the engine pre-computes
|
| 110 |
+
logits inside the graph (FlashInfer), the second element of the
|
| 111 |
+
return tuple is the logits tensor ``(B_fwd, bl, V)``.
|
| 112 |
+
"""
|
| 113 |
+
...
|
| 114 |
+
|
| 115 |
+
def advance_cache(self, block_len: int) -> None:
|
| 116 |
+
"""Commit ``block_len`` newly-written entries to the cache."""
|
| 117 |
+
...
|
| 118 |
+
|
| 119 |
+
def set_shift_ctx(self, ctx: Tensor) -> None:
|
| 120 |
+
"""Stage the SOS / previous-block-end hidden state for the graph.
|
| 121 |
+
|
| 122 |
+
FlashInfer copies this into a graph-captured buffer at the start
|
| 123 |
+
of each block; the SDPA engine ignores it (it computes the shift
|
| 124 |
+
in-line during :meth:`block_forward`).
|
| 125 |
+
"""
|
| 126 |
+
...
|
| 127 |
+
|
| 128 |
+
def capture_cuda_graph(
|
| 129 |
+
self,
|
| 130 |
+
block_size: int,
|
| 131 |
+
*,
|
| 132 |
+
speech_head: Optional[torch.nn.Linear] = None,
|
| 133 |
+
) -> None:
|
| 134 |
+
"""Capture the per-block forward as a CUDA graph.
|
| 135 |
+
|
| 136 |
+
``block_size`` is the fixed ``D`` the graph is captured for.
|
| 137 |
+
Engines that don't support CUDA graphs should leave
|
| 138 |
+
``has_cuda_graph = False``. Optionally folds the speech head
|
| 139 |
+
into the graph if supplied.
|
| 140 |
+
"""
|
| 141 |
+
...
|
runtime/chatterbox-flash/chatterbox_flash/engines/flashinfer.py
ADDED
|
@@ -0,0 +1,1656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FlashInfer attention engine with **paged KV cache** for batched
|
| 3 |
+
block-diffusion inference.
|
| 4 |
+
|
| 5 |
+
Supports ``batch_size >= 1`` via ``BatchPrefillWithPagedKVCacheWrapper``.
|
| 6 |
+
Each sample gets a static slice of pages in the pool; prefix pages are
|
| 7 |
+
computed once and replicated for same-text multi-generation, or computed
|
| 8 |
+
independently per sample for cross-text batching.
|
| 9 |
+
|
| 10 |
+
KV cache layout (NHD, per layer):
|
| 11 |
+
``(total_pages, 2, page_size, num_kv_heads, head_dim)``
|
| 12 |
+
|
| 13 |
+
Static page allocation: sample *i* → pages
|
| 14 |
+
``[i * pages_per_sample : (i+1) * pages_per_sample]``.
|
| 15 |
+
|
| 16 |
+
Fused projections (same as before):
|
| 17 |
+
- QKV: 1 GEMM per layer instead of 3.
|
| 18 |
+
- Gate-up MLP: 1 GEMM instead of 2 (Llama SwiGLU).
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import logging
|
| 24 |
+
import math
|
| 25 |
+
from typing import List, Protocol, runtime_checkable
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
from torch import Tensor
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
import flashinfer
|
| 35 |
+
_FLASHINFER_AVAILABLE = True
|
| 36 |
+
# Module-level alias avoids a name collision with the engine's
|
| 37 |
+
# ``self._fi_fused_add_rmsnorm`` helper, which forwards through to the
|
| 38 |
+
# real flashinfer kernel.
|
| 39 |
+
_flashinfer_fused_add_rmsnorm = flashinfer.norm.fused_add_rmsnorm
|
| 40 |
+
try:
|
| 41 |
+
from flashinfer.rope import apply_rope_inplace as _fi_apply_rope
|
| 42 |
+
_FI_ROPE_AVAILABLE = True
|
| 43 |
+
except (ImportError, AttributeError):
|
| 44 |
+
_FI_ROPE_AVAILABLE = False
|
| 45 |
+
except ImportError:
|
| 46 |
+
_FLASHINFER_AVAILABLE = False
|
| 47 |
+
_FI_ROPE_AVAILABLE = False
|
| 48 |
+
_flashinfer_fused_add_rmsnorm = None # type: ignore[assignment]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def flashinfer_available() -> bool:
|
| 52 |
+
return _FLASHINFER_AVAILABLE
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@runtime_checkable
|
| 56 |
+
class FlashInferProjectionProvider(Protocol):
|
| 57 |
+
"""Model-attached source of engine-local transformer projections.
|
| 58 |
+
|
| 59 |
+
A provider may own backend-prepared quantized weights instead of PyTorch
|
| 60 |
+
``Parameter`` objects. Dispatch is deliberately confined to the four
|
| 61 |
+
transformer projection boundaries in :class:`FlashInferEngine`; global
|
| 62 |
+
``torch.nn.functional.linear`` behavior is never modified.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
source_free: bool
|
| 66 |
+
|
| 67 |
+
def validate_engine(
|
| 68 |
+
self,
|
| 69 |
+
*,
|
| 70 |
+
device: torch.device,
|
| 71 |
+
dtype: torch.dtype,
|
| 72 |
+
num_layers: int,
|
| 73 |
+
) -> None:
|
| 74 |
+
"""Reject an incompatible engine before any forward executes."""
|
| 75 |
+
|
| 76 |
+
def prepare_for_engine(
|
| 77 |
+
self,
|
| 78 |
+
*,
|
| 79 |
+
batch_size: int,
|
| 80 |
+
block_size: int,
|
| 81 |
+
) -> None:
|
| 82 |
+
"""Optionally select/cache kernels for an engine shape."""
|
| 83 |
+
|
| 84 |
+
def project(
|
| 85 |
+
self,
|
| 86 |
+
kind: str,
|
| 87 |
+
layer_idx: int,
|
| 88 |
+
activation: Tensor,
|
| 89 |
+
) -> Tensor:
|
| 90 |
+
"""Apply one of ``qkv``, ``o``, ``gate_up``, or ``down``."""
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class FlashInferEngine:
|
| 94 |
+
"""FlashInfer engine with paged KV cache for batched block-diffusion."""
|
| 95 |
+
|
| 96 |
+
# ------------------------------------------------------------------ #
|
| 97 |
+
# Construction
|
| 98 |
+
# ------------------------------------------------------------------ #
|
| 99 |
+
|
| 100 |
+
def __init__(
|
| 101 |
+
self,
|
| 102 |
+
model,
|
| 103 |
+
max_seq_len: int,
|
| 104 |
+
dtype: torch.dtype,
|
| 105 |
+
batch_size: int = 1,
|
| 106 |
+
page_size: int = 16,
|
| 107 |
+
):
|
| 108 |
+
if not _FLASHINFER_AVAILABLE:
|
| 109 |
+
raise ImportError(
|
| 110 |
+
"FlashInfer not installed. Install with: "
|
| 111 |
+
"pip install flashinfer-python"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
device = model.device
|
| 115 |
+
config = model.tfmr.config
|
| 116 |
+
|
| 117 |
+
# ---- Hopper PDL (programmatic dependent launch) ----
|
| 118 |
+
# PDL lets back-to-back kernels overlap their epilogue/prologue on
|
| 119 |
+
# Hopper, which helps the memory-bound norm / activation kernels in the
|
| 120 |
+
# decoder hot loop. Auto-detected from the device capability; can be
|
| 121 |
+
# forced via ``RESEMBLETRON_FLASHINFER_PDL`` (``0`` to disable).
|
| 122 |
+
import os
|
| 123 |
+
_pdl_env = os.environ.get("RESEMBLETRON_FLASHINFER_PDL")
|
| 124 |
+
if _pdl_env is not None:
|
| 125 |
+
self._enable_pdl: bool = _pdl_env not in ("0", "false", "False")
|
| 126 |
+
elif device.type == "cuda":
|
| 127 |
+
cap = torch.cuda.get_device_capability(device)
|
| 128 |
+
self._enable_pdl = cap[0] >= 9
|
| 129 |
+
else:
|
| 130 |
+
self._enable_pdl = False
|
| 131 |
+
# ---- fp16 qk reduction for bf16 attention ----
|
| 132 |
+
# Costs one extra cast on QK but lets the tensor cores reduce in fp16,
|
| 133 |
+
# which is materially faster on Hopper. Numerically equivalent in
|
| 134 |
+
# practice for inference-time bf16 models (vLLM/SGLang default).
|
| 135 |
+
self._use_fp16_qk_red: bool = dtype in (torch.float16, torch.bfloat16)
|
| 136 |
+
|
| 137 |
+
num_layers = config.num_hidden_layers
|
| 138 |
+
num_heads = config.num_attention_heads
|
| 139 |
+
num_kv_heads = config.num_key_value_heads
|
| 140 |
+
head_dim = getattr(config, "head_dim", config.hidden_size // num_heads)
|
| 141 |
+
|
| 142 |
+
self._projection_provider: FlashInferProjectionProvider | None = getattr(
|
| 143 |
+
model, "_flashinfer_projection_provider", None,
|
| 144 |
+
)
|
| 145 |
+
if self._projection_provider is not None:
|
| 146 |
+
missing_provider_methods = [
|
| 147 |
+
name for name in ("validate_engine", "prepare_for_engine", "project")
|
| 148 |
+
if not callable(getattr(self._projection_provider, name, None))
|
| 149 |
+
]
|
| 150 |
+
if missing_provider_methods:
|
| 151 |
+
raise TypeError(
|
| 152 |
+
"model._flashinfer_projection_provider is missing callable "
|
| 153 |
+
f"methods: {missing_provider_methods}"
|
| 154 |
+
)
|
| 155 |
+
self._projection_provider.validate_engine(
|
| 156 |
+
device=device,
|
| 157 |
+
dtype=dtype,
|
| 158 |
+
num_layers=num_layers,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
B = max(1, batch_size)
|
| 162 |
+
ps = page_size
|
| 163 |
+
P = math.ceil(max_seq_len / ps)
|
| 164 |
+
total_pages = B * P
|
| 165 |
+
|
| 166 |
+
# ---- Paged KV caches ----
|
| 167 |
+
self._paged_kv: List[Tensor] = [
|
| 168 |
+
torch.zeros(
|
| 169 |
+
total_pages, 2, ps, num_kv_heads, head_dim,
|
| 170 |
+
device=device, dtype=dtype,
|
| 171 |
+
)
|
| 172 |
+
for _ in range(num_layers)
|
| 173 |
+
]
|
| 174 |
+
|
| 175 |
+
self._max_seq_len = max_seq_len
|
| 176 |
+
self._num_layers = num_layers
|
| 177 |
+
self._num_heads = num_heads
|
| 178 |
+
self._num_kv_heads = num_kv_heads
|
| 179 |
+
self._head_dim = head_dim
|
| 180 |
+
self._device = device
|
| 181 |
+
self._dtype = dtype
|
| 182 |
+
self._batch_size = B
|
| 183 |
+
self._page_size = ps
|
| 184 |
+
self._pages_per_sample = P
|
| 185 |
+
|
| 186 |
+
q_dim = num_heads * head_dim
|
| 187 |
+
kv_dim = num_kv_heads * head_dim
|
| 188 |
+
self._q_dim = q_dim
|
| 189 |
+
self._kv_dim = kv_dim
|
| 190 |
+
hidden_size = num_heads * head_dim
|
| 191 |
+
self._hidden_size = hidden_size
|
| 192 |
+
|
| 193 |
+
# ---- Fused projection weights ----
|
| 194 |
+
self._fused_qkv_w: List[Tensor] = []
|
| 195 |
+
self._fused_gate_up_w: List[Tensor] = []
|
| 196 |
+
self._o_proj_w: List[Tensor] = []
|
| 197 |
+
self._down_proj_w: List[Tensor] = []
|
| 198 |
+
self._input_ln: list = []
|
| 199 |
+
self._post_ln: list = []
|
| 200 |
+
self._mlp_modules: list = []
|
| 201 |
+
# ---- Cached RMSNorm weight/eps for FlashInfer fused kernels ----
|
| 202 |
+
# ``flashinfer.norm.{rmsnorm, fused_add_rmsnorm}`` needs the raw weight
|
| 203 |
+
# tensor + scalar eps. Caching avoids re-fetching attrs in the hot loop.
|
| 204 |
+
self._input_ln_w: List[Tensor] = []
|
| 205 |
+
self._input_ln_eps: List[float] = []
|
| 206 |
+
self._post_ln_w: List[Tensor] = []
|
| 207 |
+
self._post_ln_eps: List[float] = []
|
| 208 |
+
|
| 209 |
+
for layer in model.tfmr.layers:
|
| 210 |
+
attn = layer.self_attn
|
| 211 |
+
mlp = layer.mlp
|
| 212 |
+
if self._projection_provider is None:
|
| 213 |
+
self._fused_qkv_w.append(
|
| 214 |
+
torch.cat(
|
| 215 |
+
[attn.q_proj.weight.data, attn.k_proj.weight.data,
|
| 216 |
+
attn.v_proj.weight.data], dim=0,
|
| 217 |
+
)
|
| 218 |
+
)
|
| 219 |
+
self._o_proj_w.append(attn.o_proj.weight)
|
| 220 |
+
if hasattr(mlp, "gate_proj") and hasattr(mlp, "up_proj"):
|
| 221 |
+
self._fused_gate_up_w.append(
|
| 222 |
+
torch.cat(
|
| 223 |
+
[mlp.gate_proj.weight.data, mlp.up_proj.weight.data],
|
| 224 |
+
dim=0,
|
| 225 |
+
)
|
| 226 |
+
)
|
| 227 |
+
self._down_proj_w.append(mlp.down_proj.weight)
|
| 228 |
+
self._input_ln.append(layer.input_layernorm)
|
| 229 |
+
self._post_ln.append(layer.post_attention_layernorm)
|
| 230 |
+
self._mlp_modules.append(mlp)
|
| 231 |
+
self._input_ln_w.append(layer.input_layernorm.weight.data)
|
| 232 |
+
self._input_ln_eps.append(
|
| 233 |
+
float(layer.input_layernorm.variance_epsilon)
|
| 234 |
+
)
|
| 235 |
+
self._post_ln_w.append(
|
| 236 |
+
layer.post_attention_layernorm.weight.data
|
| 237 |
+
)
|
| 238 |
+
self._post_ln_eps.append(
|
| 239 |
+
float(layer.post_attention_layernorm.variance_epsilon)
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
self._mlp_act_fn = model.tfmr.layers[0].mlp.act_fn
|
| 243 |
+
self._final_norm = model.tfmr.norm
|
| 244 |
+
self._final_norm_w = model.tfmr.norm.weight.data
|
| 245 |
+
self._final_norm_eps = float(model.tfmr.norm.variance_epsilon)
|
| 246 |
+
# FlashInfer rmsnorm kernels support fp16 / bf16. fp32 falls back to the
|
| 247 |
+
# legacy LlamaRMSNorm module path.
|
| 248 |
+
self._use_fused_norm: bool = dtype in (torch.float16, torch.bfloat16)
|
| 249 |
+
self._use_fused_silu: bool = (
|
| 250 |
+
self._use_fused_norm
|
| 251 |
+
and (
|
| 252 |
+
self._projection_provider is not None
|
| 253 |
+
or bool(self._fused_gate_up_w)
|
| 254 |
+
)
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
if self._projection_provider is not None:
|
| 258 |
+
self._projection_provider.prepare_for_engine(
|
| 259 |
+
batch_size=B,
|
| 260 |
+
block_size=int(model.drf_block_size),
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# ---- RoPE ----
|
| 264 |
+
# transformers ≥5.9 consolidates rope settings into a single
|
| 265 |
+
# ``config.rope_parameters`` dict and DROPS the top-level
|
| 266 |
+
# ``config.rope_theta`` / ``config.rope_scaling`` attributes; older
|
| 267 |
+
# versions expose them at top level. Read both so we get the real
|
| 268 |
+
# values regardless of layout (the T3 backbone uses theta=500000 with
|
| 269 |
+
# llama3 scaling, both of which now live only in rope_parameters).
|
| 270 |
+
_rope_params = getattr(config, "rope_parameters", None) or {}
|
| 271 |
+
self._rope_theta: float = float(
|
| 272 |
+
getattr(config, "rope_theta", None)
|
| 273 |
+
or _rope_params.get("rope_theta")
|
| 274 |
+
or 10000.0
|
| 275 |
+
)
|
| 276 |
+
# ``flashinfer.rope.apply_rope_inplace`` only implements plain-theta
|
| 277 |
+
# RoPE (``rope_scale=1.0``); it does NOT apply llama3-style frequency
|
| 278 |
+
# scaling. With llama3 scaling the inplace kernel encodes wrong
|
| 279 |
+
# positions and the model degenerates into repetitive output. When any
|
| 280 |
+
# non-plain rope_type is configured, fall back to the precomputed HF
|
| 281 |
+
# cos/sin table below (built from ``model.tfmr.rotary_emb``, which bakes
|
| 282 |
+
# in the scaling) — exactly matching the torch_sdpa path.
|
| 283 |
+
_rope_scaling = getattr(config, "rope_scaling", None) or _rope_params or None
|
| 284 |
+
# transformers has used both "rope_type" (≥4.40) and the legacy "type"
|
| 285 |
+
# key; accept either, and treat anything other than default/linear
|
| 286 |
+
# (e.g. "llama3", "dynamic", "yarn") as needing the table fallback.
|
| 287 |
+
_rope_type = (
|
| 288 |
+
(_rope_scaling.get("rope_type") or _rope_scaling.get("type"))
|
| 289 |
+
if _rope_scaling
|
| 290 |
+
else None
|
| 291 |
+
)
|
| 292 |
+
_plain_rope = _rope_type in (None, "default", "linear")
|
| 293 |
+
self._use_rope_inplace: bool = (
|
| 294 |
+
_FI_ROPE_AVAILABLE and device.type == "cuda" and _plain_rope
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
if self._use_rope_inplace:
|
| 298 |
+
self._rope_indptr = torch.zeros(
|
| 299 |
+
B + 1, dtype=torch.int32, device=device,
|
| 300 |
+
)
|
| 301 |
+
self._rope_offset = torch.zeros(
|
| 302 |
+
B, dtype=torch.int32, device=device,
|
| 303 |
+
)
|
| 304 |
+
self._rope_indptr_1 = torch.zeros(
|
| 305 |
+
2, dtype=torch.int32, device=device,
|
| 306 |
+
)
|
| 307 |
+
self._rope_offset_1 = torch.zeros(
|
| 308 |
+
1, dtype=torch.int32, device=device,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
dim = config.hidden_size
|
| 312 |
+
_dummy = torch.zeros(1, 1, dim, device=device, dtype=dtype)
|
| 313 |
+
_full_pos = torch.arange(max_seq_len, device=device).unsqueeze(0)
|
| 314 |
+
self._rope_cos, self._rope_sin = model.tfmr.rotary_emb(
|
| 315 |
+
_dummy, _full_pos,
|
| 316 |
+
)
|
| 317 |
+
self._rope_cos = self._rope_cos.detach()
|
| 318 |
+
self._rope_sin = self._rope_sin.detach()
|
| 319 |
+
|
| 320 |
+
from transformers.models.llama.modeling_llama import (
|
| 321 |
+
apply_rotary_pos_emb,
|
| 322 |
+
)
|
| 323 |
+
self._apply_rotary_pos_emb = apply_rotary_pos_emb
|
| 324 |
+
|
| 325 |
+
# ---- Per-sample cache state ----
|
| 326 |
+
self._cache_lens: List[int] = [0] * B
|
| 327 |
+
self._prefix_lens: List[int] = [0] * B
|
| 328 |
+
|
| 329 |
+
# ---- Eager paged wrapper (block forward without CUDA graph) ----
|
| 330 |
+
self._fi_workspace = torch.empty(
|
| 331 |
+
256 * 1024 * 1024, dtype=torch.uint8, device=device,
|
| 332 |
+
)
|
| 333 |
+
self._fi_eager_wrapper = flashinfer.BatchPrefillWithPagedKVCacheWrapper(
|
| 334 |
+
self._fi_workspace, kv_layout="NHD",
|
| 335 |
+
)
|
| 336 |
+
# ---- Ragged prefill wrapper (cross-text prefix forward, eager only) ----
|
| 337 |
+
# Used to fuse per-sample ``single_prefill_with_kv_cache`` calls into one
|
| 338 |
+
# ragged-batched prefill per layer. Has its own workspace so its plan
|
| 339 |
+
# state cannot clobber the paged wrapper.
|
| 340 |
+
self._fi_ragged_workspace = torch.empty(
|
| 341 |
+
64 * 1024 * 1024, dtype=torch.uint8, device=device,
|
| 342 |
+
)
|
| 343 |
+
self._fi_ragged_wrapper = (
|
| 344 |
+
flashinfer.BatchPrefillWithRaggedKVCacheWrapper(
|
| 345 |
+
self._fi_ragged_workspace, kv_layout="NHD",
|
| 346 |
+
)
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
# ---- CUDA graph state ----
|
| 350 |
+
self._use_cuda_graph: bool = False
|
| 351 |
+
self._cuda_graph = None
|
| 352 |
+
self._graph_pool = None
|
| 353 |
+
self._g_block_size: int = 0
|
| 354 |
+
self._g_inputs_embeds: Tensor | None = None
|
| 355 |
+
self._g_output_hidden: Tensor | None = None
|
| 356 |
+
self._g_cos: Tensor | None = None
|
| 357 |
+
self._g_sin: Tensor | None = None
|
| 358 |
+
self._fi_graph_wrapper = None
|
| 359 |
+
self._g_page_write_idx: Tensor | None = None
|
| 360 |
+
self._g_page_write_off: Tensor | None = None
|
| 361 |
+
self._last_plan_cache_start: int | tuple = -1
|
| 362 |
+
self._plan_qo_indptr: Tensor | None = None
|
| 363 |
+
self._plan_paged_kv_indptr: Tensor | None = None
|
| 364 |
+
self._plan_paged_kv_indices: Tensor | None = None
|
| 365 |
+
self._plan_paged_kv_last_page_len: Tensor | None = None
|
| 366 |
+
|
| 367 |
+
# ---- Speech head in graph (optional) ----
|
| 368 |
+
self._has_speech_head: bool = False
|
| 369 |
+
self._speech_head_w: Tensor | None = None
|
| 370 |
+
self._speech_head_b: Tensor | None = None
|
| 371 |
+
self._g_shift_ctx: Tensor | None = None
|
| 372 |
+
self._g_logits: Tensor | None = None
|
| 373 |
+
|
| 374 |
+
logger.info(
|
| 375 |
+
"FlashInferEngine: max_seq=%d layers=%d heads=%d/%d dim=%d "
|
| 376 |
+
"batch=%d page_size=%d pages/sample=%d total_pages=%d "
|
| 377 |
+
"rope_inplace=%s dtype=%s",
|
| 378 |
+
max_seq_len, num_layers, num_heads, num_kv_heads, head_dim,
|
| 379 |
+
B, ps, P, total_pages, self._use_rope_inplace, dtype,
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
# ------------------------------------------------------------------ #
|
| 383 |
+
# Page-index helpers
|
| 384 |
+
# ------------------------------------------------------------------ #
|
| 385 |
+
|
| 386 |
+
def _compute_page_write_indices(
|
| 387 |
+
self, cache_starts: int | List[int], bl: int,
|
| 388 |
+
) -> tuple[Tensor, Tensor]:
|
| 389 |
+
"""Flat page indices + offsets for writing ``B * bl`` KV entries."""
|
| 390 |
+
B = self._batch_size
|
| 391 |
+
P = self._pages_per_sample
|
| 392 |
+
ps = self._page_size
|
| 393 |
+
dev = self._device
|
| 394 |
+
|
| 395 |
+
if isinstance(cache_starts, int):
|
| 396 |
+
pos = torch.arange(cache_starts, cache_starts + bl, device=dev)
|
| 397 |
+
p_in_s = pos // ps
|
| 398 |
+
off = pos % ps
|
| 399 |
+
base = torch.arange(B, device=dev, dtype=torch.long) * P
|
| 400 |
+
return (
|
| 401 |
+
(base[:, None] + p_in_s[None, :]).reshape(-1),
|
| 402 |
+
off.repeat(B),
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
parts_idx: list[Tensor] = []
|
| 406 |
+
parts_off: list[Tensor] = []
|
| 407 |
+
for bi in range(B):
|
| 408 |
+
cs = cache_starts[bi]
|
| 409 |
+
pos = torch.arange(cs, cs + bl, device=dev)
|
| 410 |
+
parts_idx.append(bi * P + pos // ps)
|
| 411 |
+
parts_off.append(pos % ps)
|
| 412 |
+
return torch.cat(parts_idx), torch.cat(parts_off)
|
| 413 |
+
|
| 414 |
+
def _plan_paged_attention(
|
| 415 |
+
self,
|
| 416 |
+
wrapper,
|
| 417 |
+
kv_lens: int | List[int],
|
| 418 |
+
seq_len: int,
|
| 419 |
+
causal: bool = False,
|
| 420 |
+
*,
|
| 421 |
+
qo_indptr_buf: Tensor | None = None,
|
| 422 |
+
paged_kv_indptr_buf: Tensor | None = None,
|
| 423 |
+
paged_kv_indices_buf: Tensor | None = None,
|
| 424 |
+
paged_kv_last_page_len_buf: Tensor | None = None,
|
| 425 |
+
):
|
| 426 |
+
"""Call ``wrapper.plan()`` with page-table metadata."""
|
| 427 |
+
B = self._batch_size
|
| 428 |
+
P = self._pages_per_sample
|
| 429 |
+
ps = self._page_size
|
| 430 |
+
dev = self._device
|
| 431 |
+
|
| 432 |
+
kv_list = [kv_lens] * B if isinstance(kv_lens, int) else list(kv_lens)
|
| 433 |
+
pages_used = [math.ceil(kl / ps) for kl in kv_list]
|
| 434 |
+
total_pu = sum(pages_used)
|
| 435 |
+
|
| 436 |
+
use_buf = qo_indptr_buf is not None
|
| 437 |
+
|
| 438 |
+
qo = qo_indptr_buf if use_buf else torch.zeros(
|
| 439 |
+
B + 1, dtype=torch.int32, device=dev,
|
| 440 |
+
)
|
| 441 |
+
kvi = paged_kv_indptr_buf if use_buf else torch.zeros(
|
| 442 |
+
B + 1, dtype=torch.int32, device=dev,
|
| 443 |
+
)
|
| 444 |
+
kvidx = paged_kv_indices_buf if use_buf else torch.empty(
|
| 445 |
+
total_pu, dtype=torch.int32, device=dev,
|
| 446 |
+
)
|
| 447 |
+
kvlp = paged_kv_last_page_len_buf if use_buf else torch.empty(
|
| 448 |
+
B, dtype=torch.int32, device=dev,
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
if use_buf:
|
| 452 |
+
qo[0] = 0
|
| 453 |
+
kvi[0] = 0
|
| 454 |
+
|
| 455 |
+
idx = 0
|
| 456 |
+
for bi in range(B):
|
| 457 |
+
pu = pages_used[bi]
|
| 458 |
+
qo[bi + 1] = qo[bi] + seq_len
|
| 459 |
+
kvi[bi + 1] = kvi[bi] + pu
|
| 460 |
+
kvidx[idx: idx + pu] = torch.arange(
|
| 461 |
+
bi * P, bi * P + pu, dtype=torch.int32, device=dev,
|
| 462 |
+
)
|
| 463 |
+
idx += pu
|
| 464 |
+
last = kv_list[bi] % ps
|
| 465 |
+
kvlp[bi] = last if last > 0 else ps
|
| 466 |
+
|
| 467 |
+
wrapper.plan(
|
| 468 |
+
qo_indptr=qo,
|
| 469 |
+
paged_kv_indptr=kvi,
|
| 470 |
+
paged_kv_indices=kvidx[:total_pu],
|
| 471 |
+
paged_kv_last_page_len=kvlp,
|
| 472 |
+
num_qo_heads=self._num_heads,
|
| 473 |
+
num_kv_heads=self._num_kv_heads,
|
| 474 |
+
head_dim_qk=self._head_dim,
|
| 475 |
+
page_size=ps,
|
| 476 |
+
causal=causal,
|
| 477 |
+
q_data_type=self._dtype,
|
| 478 |
+
use_fp16_qk_reduction=self._use_fp16_qk_red,
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
# ------------------------------------------------------------------ #
|
| 482 |
+
# MLP helper (shared by prefix + block + graph paths)
|
| 483 |
+
# ------------------------------------------------------------------ #
|
| 484 |
+
|
| 485 |
+
def _project(
|
| 486 |
+
self,
|
| 487 |
+
kind: str,
|
| 488 |
+
layer_idx: int,
|
| 489 |
+
activation: Tensor,
|
| 490 |
+
) -> Tensor:
|
| 491 |
+
"""Apply an engine transformer projection at its explicit boundary."""
|
| 492 |
+
if self._projection_provider is not None:
|
| 493 |
+
return self._projection_provider.project(
|
| 494 |
+
kind, layer_idx, activation,
|
| 495 |
+
)
|
| 496 |
+
weights = {
|
| 497 |
+
"qkv": self._fused_qkv_w,
|
| 498 |
+
"o": self._o_proj_w,
|
| 499 |
+
"gate_up": self._fused_gate_up_w,
|
| 500 |
+
"down": self._down_proj_w,
|
| 501 |
+
}
|
| 502 |
+
try:
|
| 503 |
+
weight = weights[kind][layer_idx]
|
| 504 |
+
except (KeyError, IndexError) as exc:
|
| 505 |
+
raise RuntimeError(
|
| 506 |
+
f"Missing {kind} projection for transformer layer {layer_idx}"
|
| 507 |
+
) from exc
|
| 508 |
+
return F.linear(activation, weight)
|
| 509 |
+
|
| 510 |
+
# ------------------------------------------------------------------ #
|
| 511 |
+
# FlashInfer kernel wrappers (forward PDL flag once)
|
| 512 |
+
# ------------------------------------------------------------------ #
|
| 513 |
+
|
| 514 |
+
def _fi_rmsnorm(self, x: Tensor, w: Tensor, eps: float) -> Tensor:
|
| 515 |
+
return flashinfer.norm.rmsnorm(
|
| 516 |
+
x, w, eps, enable_pdl=self._enable_pdl,
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
def _fi_fused_add_rmsnorm(
|
| 520 |
+
self, x: Tensor, residual: Tensor, w: Tensor, eps: float,
|
| 521 |
+
) -> None:
|
| 522 |
+
_flashinfer_fused_add_rmsnorm(
|
| 523 |
+
x, residual, w, eps, enable_pdl=self._enable_pdl,
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
def _mlp_forward(self, layer_idx: int, hidden: Tensor) -> Tensor:
|
| 527 |
+
"""Llama SwiGLU MLP. Accepts any leading dims ``(..., dim)``.
|
| 528 |
+
|
| 529 |
+
When fused gate-up weight + bf16/fp16 are available, the
|
| 530 |
+
``silu(gate) * up`` elementwise op is replaced by FlashInfer's
|
| 531 |
+
``silu_and_mul`` (single fused kernel, 1 read + 1 write of the
|
| 532 |
+
intermediate tensor instead of 2 reads + 1 write).
|
| 533 |
+
"""
|
| 534 |
+
if self._projection_provider is None and not self._fused_gate_up_w:
|
| 535 |
+
return self._mlp_modules[layer_idx](hidden)
|
| 536 |
+
|
| 537 |
+
gate_up = self._project("gate_up", layer_idx, hidden)
|
| 538 |
+
if self._use_fused_silu:
|
| 539 |
+
orig_shape = gate_up.shape
|
| 540 |
+
gate_up_2d = gate_up.reshape(-1, orig_shape[-1])
|
| 541 |
+
act_2d = flashinfer.activation.silu_and_mul(
|
| 542 |
+
gate_up_2d, enable_pdl=self._enable_pdl,
|
| 543 |
+
)
|
| 544 |
+
act = act_2d.view(*orig_shape[:-1], act_2d.size(-1))
|
| 545 |
+
else:
|
| 546 |
+
mid = gate_up.size(-1) // 2
|
| 547 |
+
act = self._mlp_act_fn(gate_up[..., :mid]) * gate_up[..., mid:]
|
| 548 |
+
return self._project("down", layer_idx, act)
|
| 549 |
+
|
| 550 |
+
# ------------------------------------------------------------------ #
|
| 551 |
+
# Prefix forward (single-request per sample, paged KV write)
|
| 552 |
+
# ------------------------------------------------------------------ #
|
| 553 |
+
|
| 554 |
+
def prefix_forward(
|
| 555 |
+
self,
|
| 556 |
+
prefix_emb_or_list: Tensor | List[Tensor],
|
| 557 |
+
prefix_lens: List[int] | None = None,
|
| 558 |
+
) -> Tensor:
|
| 559 |
+
"""Compute prefix KV and store in paged cache.
|
| 560 |
+
|
| 561 |
+
Same-text mode
|
| 562 |
+
``prefix_emb_or_list`` is ``(1, lp, dim)``.
|
| 563 |
+
Computed once for sample 0, pages copied to samples 1..B-1.
|
| 564 |
+
|
| 565 |
+
Cross-text mode
|
| 566 |
+
``prefix_emb_or_list`` is a list of ``(1, lp_i, dim)``.
|
| 567 |
+
Each sample processed independently.
|
| 568 |
+
|
| 569 |
+
Returns:
|
| 570 |
+
``(B, 1, dim)`` shift context (SOS hidden per sample).
|
| 571 |
+
"""
|
| 572 |
+
B = self._batch_size
|
| 573 |
+
|
| 574 |
+
if isinstance(prefix_emb_or_list, list):
|
| 575 |
+
# Cross-text: pack all samples into one ragged batched prefill so
|
| 576 |
+
# we issue O(L) kernel launches instead of O(L * B).
|
| 577 |
+
return self._prefix_forward_batched(prefix_emb_or_list)
|
| 578 |
+
|
| 579 |
+
emb = prefix_emb_or_list[:1] if prefix_emb_or_list.size(0) > 1 else prefix_emb_or_list
|
| 580 |
+
ctx = self._prefix_forward_one(emb, sample_idx=0)
|
| 581 |
+
lp = prefix_emb_or_list.size(1)
|
| 582 |
+
pfx_pages = math.ceil(lp / self._page_size)
|
| 583 |
+
P = self._pages_per_sample
|
| 584 |
+
for li in range(self._num_layers):
|
| 585 |
+
src = self._paged_kv[li][:pfx_pages]
|
| 586 |
+
for bi in range(1, B):
|
| 587 |
+
s = bi * P
|
| 588 |
+
self._paged_kv[li][s: s + pfx_pages] = src
|
| 589 |
+
for bi in range(1, B):
|
| 590 |
+
self._cache_lens[bi] = lp
|
| 591 |
+
self._prefix_lens[bi] = lp
|
| 592 |
+
return ctx.expand(B, -1, -1).contiguous()
|
| 593 |
+
|
| 594 |
+
def _prefix_forward_one(
|
| 595 |
+
self, prefix_emb: Tensor, sample_idx: int,
|
| 596 |
+
) -> Tensor:
|
| 597 |
+
"""Single-sample prefix prefill, writes into ``sample_idx`` page slice.
|
| 598 |
+
|
| 599 |
+
Decoder body uses the vLLM-style ``(hidden, residual)`` carry pattern
|
| 600 |
+
with :func:`flashinfer.norm.fused_add_rmsnorm` (in-place residual add
|
| 601 |
+
+ RMSNorm in a single kernel). Attention is the optimized
|
| 602 |
+
:func:`flashinfer.single_prefill_with_kv_cache` (causal).
|
| 603 |
+
"""
|
| 604 |
+
lp = prefix_emb.size(1)
|
| 605 |
+
page_off = sample_idx * self._pages_per_sample
|
| 606 |
+
ps = self._page_size
|
| 607 |
+
dev = self._device
|
| 608 |
+
|
| 609 |
+
pos = torch.arange(lp, device=dev)
|
| 610 |
+
pw_idx = page_off + pos // ps
|
| 611 |
+
pw_off = pos % ps
|
| 612 |
+
|
| 613 |
+
if self._use_rope_inplace:
|
| 614 |
+
self._rope_indptr_1[1] = lp
|
| 615 |
+
self._rope_offset_1[0] = 0
|
| 616 |
+
|
| 617 |
+
nh = self._num_heads
|
| 618 |
+
nkv = self._num_kv_heads
|
| 619 |
+
hd = self._head_dim
|
| 620 |
+
qd = self._q_dim
|
| 621 |
+
kvd = self._kv_dim
|
| 622 |
+
|
| 623 |
+
if self._use_fused_norm:
|
| 624 |
+
return self._prefix_forward_one_fused(
|
| 625 |
+
prefix_emb, sample_idx, lp, pw_idx, pw_off,
|
| 626 |
+
nh, nkv, hd, qd, kvd,
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
hidden = prefix_emb
|
| 630 |
+
|
| 631 |
+
for li in range(self._num_layers):
|
| 632 |
+
residual = hidden
|
| 633 |
+
hidden = self._input_ln[li](hidden)
|
| 634 |
+
|
| 635 |
+
qkv = self._project("qkv", li, hidden)
|
| 636 |
+
q = qkv[:, :, :qd].view(1, lp, nh, hd)
|
| 637 |
+
k = qkv[:, :, qd: qd + kvd].view(1, lp, nkv, hd)
|
| 638 |
+
v = qkv[:, :, qd + kvd:].view(1, lp, nkv, hd)
|
| 639 |
+
|
| 640 |
+
qs = q.squeeze(0)
|
| 641 |
+
ks = k.squeeze(0)
|
| 642 |
+
vs = v.squeeze(0)
|
| 643 |
+
|
| 644 |
+
if self._use_rope_inplace:
|
| 645 |
+
_fi_apply_rope(
|
| 646 |
+
qs, ks, self._rope_indptr_1, self._rope_offset_1,
|
| 647 |
+
interleave=False, rope_scale=1.0,
|
| 648 |
+
rope_theta=self._rope_theta,
|
| 649 |
+
)
|
| 650 |
+
else:
|
| 651 |
+
cos = self._rope_cos[:, :lp, :]
|
| 652 |
+
sin = self._rope_sin[:, :lp, :]
|
| 653 |
+
q, k = self._apply_rotary_pos_emb(
|
| 654 |
+
q, k, cos, sin, unsqueeze_dim=2,
|
| 655 |
+
)
|
| 656 |
+
qs = q.squeeze(0)
|
| 657 |
+
ks = k.squeeze(0)
|
| 658 |
+
|
| 659 |
+
attn_out = flashinfer.single_prefill_with_kv_cache(
|
| 660 |
+
qs, ks, vs, causal=True, kv_layout="NHD",
|
| 661 |
+
use_fp16_qk_reduction=self._use_fp16_qk_red,
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
self._paged_kv[li][pw_idx, 0, pw_off] = ks
|
| 665 |
+
self._paged_kv[li][pw_idx, 1, pw_off] = vs
|
| 666 |
+
|
| 667 |
+
attn_out = attn_out.unsqueeze(0).reshape(1, lp, -1)
|
| 668 |
+
attn_out = self._project("o", li, attn_out)
|
| 669 |
+
|
| 670 |
+
hidden = residual + attn_out
|
| 671 |
+
residual = hidden
|
| 672 |
+
hidden = self._post_ln[li](hidden)
|
| 673 |
+
hidden = self._mlp_forward(li, hidden)
|
| 674 |
+
hidden = residual + hidden
|
| 675 |
+
|
| 676 |
+
hidden = self._final_norm(hidden)
|
| 677 |
+
self._cache_lens[sample_idx] = lp
|
| 678 |
+
self._prefix_lens[sample_idx] = lp
|
| 679 |
+
return hidden[:, lp - 1: lp, :]
|
| 680 |
+
|
| 681 |
+
def _prefix_forward_one_fused(
|
| 682 |
+
self,
|
| 683 |
+
prefix_emb: Tensor,
|
| 684 |
+
sample_idx: int,
|
| 685 |
+
lp: int,
|
| 686 |
+
pw_idx: Tensor,
|
| 687 |
+
pw_off: Tensor,
|
| 688 |
+
nh: int,
|
| 689 |
+
nkv: int,
|
| 690 |
+
hd: int,
|
| 691 |
+
qd: int,
|
| 692 |
+
kvd: int,
|
| 693 |
+
) -> Tensor:
|
| 694 |
+
"""fused_add_rmsnorm + silu_and_mul variant of :meth:`_prefix_forward_one`."""
|
| 695 |
+
L = self._num_layers
|
| 696 |
+
# Work in 2D ``(lp, dim)`` so fused_add_rmsnorm (which wants 2D contig)
|
| 697 |
+
# and silu_and_mul (any leading dims) both apply cleanly.
|
| 698 |
+
# Clone so we don't mutate the caller's ``prefix_emb``.
|
| 699 |
+
residual = prefix_emb.reshape(lp, -1).contiguous().clone()
|
| 700 |
+
hidden = self._fi_rmsnorm(
|
| 701 |
+
residual, self._input_ln_w[0], self._input_ln_eps[0],
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
for li in range(L):
|
| 705 |
+
qkv = self._project("qkv", li, hidden)
|
| 706 |
+
qs = qkv[:, :qd].view(lp, nh, hd)
|
| 707 |
+
ks = qkv[:, qd: qd + kvd].view(lp, nkv, hd)
|
| 708 |
+
vs = qkv[:, qd + kvd:].view(lp, nkv, hd)
|
| 709 |
+
|
| 710 |
+
if self._use_rope_inplace:
|
| 711 |
+
_fi_apply_rope(
|
| 712 |
+
qs, ks, self._rope_indptr_1, self._rope_offset_1,
|
| 713 |
+
interleave=False, rope_scale=1.0,
|
| 714 |
+
rope_theta=self._rope_theta,
|
| 715 |
+
)
|
| 716 |
+
else:
|
| 717 |
+
# Apply RoPE via HF helper (expects ``(B, S, h, hd)``).
|
| 718 |
+
q3 = qs.unsqueeze(0)
|
| 719 |
+
k3 = ks.unsqueeze(0)
|
| 720 |
+
cos = self._rope_cos[:, :lp, :]
|
| 721 |
+
sin = self._rope_sin[:, :lp, :]
|
| 722 |
+
q3, k3 = self._apply_rotary_pos_emb(
|
| 723 |
+
q3, k3, cos, sin, unsqueeze_dim=2,
|
| 724 |
+
)
|
| 725 |
+
qs = q3.squeeze(0)
|
| 726 |
+
ks = k3.squeeze(0)
|
| 727 |
+
|
| 728 |
+
attn_out = flashinfer.single_prefill_with_kv_cache(
|
| 729 |
+
qs, ks, vs, causal=True, kv_layout="NHD",
|
| 730 |
+
use_fp16_qk_reduction=self._use_fp16_qk_red,
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
self._paged_kv[li][pw_idx, 0, pw_off] = ks
|
| 734 |
+
self._paged_kv[li][pw_idx, 1, pw_off] = vs
|
| 735 |
+
|
| 736 |
+
attn_out_2d = attn_out.reshape(lp, -1)
|
| 737 |
+
attn_out_2d = self._project("o", li, attn_out_2d).contiguous()
|
| 738 |
+
|
| 739 |
+
# residual += attn_out; attn_out = rmsnorm(residual, post_ln_w)
|
| 740 |
+
self._fi_fused_add_rmsnorm(
|
| 741 |
+
attn_out_2d, residual,
|
| 742 |
+
self._post_ln_w[li], self._post_ln_eps[li],
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
mlp_out = self._mlp_forward(li, attn_out_2d)
|
| 746 |
+
if not mlp_out.is_contiguous():
|
| 747 |
+
mlp_out = mlp_out.contiguous()
|
| 748 |
+
|
| 749 |
+
if li + 1 < L:
|
| 750 |
+
self._fi_fused_add_rmsnorm(
|
| 751 |
+
mlp_out, residual,
|
| 752 |
+
self._input_ln_w[li + 1], self._input_ln_eps[li + 1],
|
| 753 |
+
)
|
| 754 |
+
hidden = mlp_out
|
| 755 |
+
else:
|
| 756 |
+
residual.add_(mlp_out)
|
| 757 |
+
hidden = residual
|
| 758 |
+
|
| 759 |
+
# Final norm and per-sample SOS hidden.
|
| 760 |
+
out = self._fi_rmsnorm(
|
| 761 |
+
hidden, self._final_norm_w, self._final_norm_eps,
|
| 762 |
+
)
|
| 763 |
+
self._cache_lens[sample_idx] = lp
|
| 764 |
+
self._prefix_lens[sample_idx] = lp
|
| 765 |
+
return out[lp - 1: lp, :].view(1, 1, -1)
|
| 766 |
+
|
| 767 |
+
# ------------------------------------------------------------------ #
|
| 768 |
+
# Cross-text ragged batched prefix forward
|
| 769 |
+
# ------------------------------------------------------------------ #
|
| 770 |
+
|
| 771 |
+
def _prefix_forward_batched(
|
| 772 |
+
self, emb_list: List[Tensor],
|
| 773 |
+
) -> Tensor:
|
| 774 |
+
"""Cross-text prefix prefill via ragged batched attention.
|
| 775 |
+
|
| 776 |
+
Replaces the per-sample loop of
|
| 777 |
+
:meth:`_prefix_forward_one` with a single
|
| 778 |
+
:class:`flashinfer.BatchPrefillWithRaggedKVCacheWrapper.run`
|
| 779 |
+
per layer. Sample ``bi`` may have its own prefix length ``lp_i``;
|
| 780 |
+
embeddings are packed into a flat ``(sum_i lp_i, dim)`` tensor.
|
| 781 |
+
|
| 782 |
+
Each sample's KV is scattered into its own paged-cache slice
|
| 783 |
+
``[bi*P : bi*P + ceil(lp_i/ps)]``.
|
| 784 |
+
|
| 785 |
+
Returns:
|
| 786 |
+
``(B_actual, 1, dim)`` per-sample SOS hidden.
|
| 787 |
+
"""
|
| 788 |
+
B_actual = len(emb_list)
|
| 789 |
+
if B_actual == 1:
|
| 790 |
+
# Single sample: paged ragged setup overhead is wasteful. Reuse the
|
| 791 |
+
# well-tuned single-sample path which calls single_prefill directly.
|
| 792 |
+
return self._prefix_forward_one(emb_list[0], 0)
|
| 793 |
+
|
| 794 |
+
dev = self._device
|
| 795 |
+
ps = self._page_size
|
| 796 |
+
P = self._pages_per_sample
|
| 797 |
+
nh = self._num_heads
|
| 798 |
+
nkv = self._num_kv_heads
|
| 799 |
+
hd = self._head_dim
|
| 800 |
+
qd = self._q_dim
|
| 801 |
+
kvd = self._kv_dim
|
| 802 |
+
L = self._num_layers
|
| 803 |
+
dtype = self._dtype
|
| 804 |
+
|
| 805 |
+
lp_list = [int(e.size(1)) for e in emb_list]
|
| 806 |
+
total_lp = sum(lp_list)
|
| 807 |
+
|
| 808 |
+
# Pack embeddings to (total_lp, dim).
|
| 809 |
+
packed = torch.cat(
|
| 810 |
+
[e.reshape(-1, e.size(-1)) for e in emb_list], dim=0,
|
| 811 |
+
).to(dtype=dtype).contiguous()
|
| 812 |
+
|
| 813 |
+
# qo_indptr == kv_indptr for self-attention prefill.
|
| 814 |
+
indptr_host = [0]
|
| 815 |
+
for lp_i in lp_list:
|
| 816 |
+
indptr_host.append(indptr_host[-1] + lp_i)
|
| 817 |
+
qo_indptr = torch.tensor(
|
| 818 |
+
indptr_host, dtype=torch.int32, device=dev,
|
| 819 |
+
)
|
| 820 |
+
|
| 821 |
+
# Paged-write indices: sample bi's positions [0, lp_i) → pages
|
| 822 |
+
# [bi*P, bi*P + ceil(lp_i/ps)).
|
| 823 |
+
pw_idx_parts: list[Tensor] = []
|
| 824 |
+
pw_off_parts: list[Tensor] = []
|
| 825 |
+
for bi, lp_i in enumerate(lp_list):
|
| 826 |
+
pos = torch.arange(lp_i, device=dev)
|
| 827 |
+
pw_idx_parts.append(bi * P + pos // ps)
|
| 828 |
+
pw_off_parts.append(pos % ps)
|
| 829 |
+
pw_idx = torch.cat(pw_idx_parts)
|
| 830 |
+
pw_off = torch.cat(pw_off_parts)
|
| 831 |
+
|
| 832 |
+
# RoPE: each sample starts from position 0 (prefix). Reuse indptr as
|
| 833 |
+
# rope_indptr; offsets are all zero. ``apply_rope_inplace`` wants int32.
|
| 834 |
+
if self._use_rope_inplace:
|
| 835 |
+
rope_indptr = qo_indptr.to(torch.int32)
|
| 836 |
+
rope_offset = torch.zeros(
|
| 837 |
+
B_actual, dtype=torch.int32, device=dev,
|
| 838 |
+
)
|
| 839 |
+
else:
|
| 840 |
+
# Pre-build packed cos/sin once; shape (total_lp, head_dim).
|
| 841 |
+
cos_parts = [self._rope_cos[0, :lp_i, :] for lp_i in lp_list]
|
| 842 |
+
sin_parts = [self._rope_sin[0, :lp_i, :] for lp_i in lp_list]
|
| 843 |
+
packed_cos = torch.cat(cos_parts, dim=0)
|
| 844 |
+
packed_sin = torch.cat(sin_parts, dim=0)
|
| 845 |
+
|
| 846 |
+
# Plan ragged wrapper once for the whole layer stack (causal).
|
| 847 |
+
self._fi_ragged_wrapper.plan(
|
| 848 |
+
qo_indptr=qo_indptr,
|
| 849 |
+
kv_indptr=qo_indptr,
|
| 850 |
+
num_qo_heads=nh,
|
| 851 |
+
num_kv_heads=nkv,
|
| 852 |
+
head_dim_qk=hd,
|
| 853 |
+
causal=True,
|
| 854 |
+
q_data_type=dtype,
|
| 855 |
+
use_fp16_qk_reduction=self._use_fp16_qk_red,
|
| 856 |
+
)
|
| 857 |
+
|
| 858 |
+
use_fused = self._use_fused_norm
|
| 859 |
+
if use_fused:
|
| 860 |
+
residual = packed.clone()
|
| 861 |
+
hidden = self._fi_rmsnorm(
|
| 862 |
+
residual, self._input_ln_w[0], self._input_ln_eps[0],
|
| 863 |
+
)
|
| 864 |
+
else:
|
| 865 |
+
hidden = packed # (total_lp, dim)
|
| 866 |
+
|
| 867 |
+
for li in range(L):
|
| 868 |
+
if use_fused:
|
| 869 |
+
qkv = self._project("qkv", li, hidden)
|
| 870 |
+
else:
|
| 871 |
+
# Module-style norm requires 3D ``(1, total_lp, dim)``.
|
| 872 |
+
normed = self._input_ln[li](hidden.unsqueeze(0)).squeeze(0)
|
| 873 |
+
qkv = self._project("qkv", li, normed)
|
| 874 |
+
|
| 875 |
+
qs = qkv[:, :qd].view(total_lp, nh, hd)
|
| 876 |
+
ks = qkv[:, qd: qd + kvd].view(total_lp, nkv, hd)
|
| 877 |
+
vs = qkv[:, qd + kvd:].view(total_lp, nkv, hd)
|
| 878 |
+
|
| 879 |
+
if self._use_rope_inplace:
|
| 880 |
+
_fi_apply_rope(
|
| 881 |
+
qs, ks, rope_indptr, rope_offset,
|
| 882 |
+
interleave=False, rope_scale=1.0,
|
| 883 |
+
rope_theta=self._rope_theta,
|
| 884 |
+
)
|
| 885 |
+
else:
|
| 886 |
+
# ``apply_rotary_pos_emb`` expects ``(B, S, h, hd)``; mimic with
|
| 887 |
+
# a synthetic batch dim over the packed sequence.
|
| 888 |
+
cos = packed_cos.unsqueeze(0)
|
| 889 |
+
sin = packed_sin.unsqueeze(0)
|
| 890 |
+
q3 = qs.unsqueeze(0)
|
| 891 |
+
k3 = ks.unsqueeze(0)
|
| 892 |
+
q3, k3 = self._apply_rotary_pos_emb(
|
| 893 |
+
q3, k3, cos, sin, unsqueeze_dim=2,
|
| 894 |
+
)
|
| 895 |
+
qs = q3.squeeze(0)
|
| 896 |
+
ks = k3.squeeze(0)
|
| 897 |
+
|
| 898 |
+
self._paged_kv[li][pw_idx, 0, pw_off] = ks
|
| 899 |
+
self._paged_kv[li][pw_idx, 1, pw_off] = vs
|
| 900 |
+
|
| 901 |
+
attn_out = self._fi_ragged_wrapper.run(qs, ks, vs)
|
| 902 |
+
attn_out_2d = attn_out.reshape(total_lp, -1)
|
| 903 |
+
attn_out_2d = self._project(
|
| 904 |
+
"o", li, attn_out_2d,
|
| 905 |
+
).contiguous()
|
| 906 |
+
|
| 907 |
+
if use_fused:
|
| 908 |
+
self._fi_fused_add_rmsnorm(
|
| 909 |
+
attn_out_2d, residual,
|
| 910 |
+
self._post_ln_w[li], self._post_ln_eps[li],
|
| 911 |
+
)
|
| 912 |
+
mlp_out = self._mlp_forward(li, attn_out_2d)
|
| 913 |
+
if not mlp_out.is_contiguous():
|
| 914 |
+
mlp_out = mlp_out.contiguous()
|
| 915 |
+
if li + 1 < L:
|
| 916 |
+
self._fi_fused_add_rmsnorm(
|
| 917 |
+
mlp_out, residual,
|
| 918 |
+
self._input_ln_w[li + 1], self._input_ln_eps[li + 1],
|
| 919 |
+
)
|
| 920 |
+
hidden = mlp_out
|
| 921 |
+
else:
|
| 922 |
+
residual.add_(mlp_out)
|
| 923 |
+
hidden = residual
|
| 924 |
+
else:
|
| 925 |
+
# Non-fused fallback: keep classic residual carry.
|
| 926 |
+
hidden = hidden + attn_out_2d # residual after attn
|
| 927 |
+
residual_after_attn = hidden
|
| 928 |
+
hidden_n = self._post_ln[li](hidden.unsqueeze(0)).squeeze(0)
|
| 929 |
+
mlp_out = self._mlp_forward(li, hidden_n)
|
| 930 |
+
hidden = residual_after_attn + mlp_out
|
| 931 |
+
|
| 932 |
+
if use_fused:
|
| 933 |
+
out = self._fi_rmsnorm(
|
| 934 |
+
hidden, self._final_norm_w, self._final_norm_eps,
|
| 935 |
+
)
|
| 936 |
+
else:
|
| 937 |
+
out = self._final_norm(hidden.unsqueeze(0)).squeeze(0)
|
| 938 |
+
|
| 939 |
+
# Per-sample SOS hidden = out[lp_i - 1] within each sample's slice.
|
| 940 |
+
shift_idx = (qo_indptr[1:] - 1).to(torch.long)
|
| 941 |
+
shift_ctx = out.index_select(0, shift_idx).unsqueeze(1) # (B_actual, 1, dim)
|
| 942 |
+
|
| 943 |
+
for bi, lp_i in enumerate(lp_list):
|
| 944 |
+
self._cache_lens[bi] = lp_i
|
| 945 |
+
self._prefix_lens[bi] = lp_i
|
| 946 |
+
|
| 947 |
+
return shift_ctx.contiguous()
|
| 948 |
+
|
| 949 |
+
# ------------------------------------------------------------------ #
|
| 950 |
+
# Batched block forward (non-causal, paged attention)
|
| 951 |
+
# ------------------------------------------------------------------ #
|
| 952 |
+
|
| 953 |
+
def block_forward(
|
| 954 |
+
self,
|
| 955 |
+
block_emb: Tensor,
|
| 956 |
+
cache_starts: int | List[int],
|
| 957 |
+
) -> Tensor:
|
| 958 |
+
"""Batched block forward with non-causal paged attention.
|
| 959 |
+
|
| 960 |
+
Args:
|
| 961 |
+
block_emb: ``(B, bl, dim)``
|
| 962 |
+
cache_starts: int (uniform) or list[int] (per-sample).
|
| 963 |
+
|
| 964 |
+
Returns:
|
| 965 |
+
``(B, bl, dim)`` after final RMSNorm.
|
| 966 |
+
"""
|
| 967 |
+
B = self._batch_size
|
| 968 |
+
bl = block_emb.size(1)
|
| 969 |
+
|
| 970 |
+
pw_idx, pw_off = self._compute_page_write_indices(cache_starts, bl)
|
| 971 |
+
|
| 972 |
+
if isinstance(cache_starts, int):
|
| 973 |
+
kv_lens: int | List[int] = cache_starts + bl
|
| 974 |
+
else:
|
| 975 |
+
kv_lens = [cs + bl for cs in cache_starts]
|
| 976 |
+
|
| 977 |
+
self._plan_paged_attention(
|
| 978 |
+
self._fi_eager_wrapper, kv_lens, seq_len=bl, causal=False,
|
| 979 |
+
)
|
| 980 |
+
|
| 981 |
+
if self._use_rope_inplace:
|
| 982 |
+
for bi in range(B):
|
| 983 |
+
self._rope_indptr[bi + 1] = (bi + 1) * bl
|
| 984 |
+
if isinstance(cache_starts, int):
|
| 985 |
+
self._rope_offset[:] = cache_starts
|
| 986 |
+
else:
|
| 987 |
+
for bi in range(B):
|
| 988 |
+
self._rope_offset[bi] = cache_starts[bi]
|
| 989 |
+
|
| 990 |
+
nh = self._num_heads
|
| 991 |
+
nkv = self._num_kv_heads
|
| 992 |
+
hd = self._head_dim
|
| 993 |
+
qd = self._q_dim
|
| 994 |
+
kvd = self._kv_dim
|
| 995 |
+
|
| 996 |
+
if self._use_fused_norm:
|
| 997 |
+
return self._block_forward_fused(
|
| 998 |
+
block_emb, cache_starts, B, bl, pw_idx, pw_off,
|
| 999 |
+
nh, nkv, hd, qd, kvd,
|
| 1000 |
+
)
|
| 1001 |
+
|
| 1002 |
+
hidden = block_emb
|
| 1003 |
+
|
| 1004 |
+
for li in range(self._num_layers):
|
| 1005 |
+
residual = hidden
|
| 1006 |
+
hidden = self._input_ln[li](hidden)
|
| 1007 |
+
|
| 1008 |
+
qkv = self._project("qkv", li, hidden)
|
| 1009 |
+
q = qkv[:, :, :qd].view(B, bl, nh, hd)
|
| 1010 |
+
k = qkv[:, :, qd: qd + kvd].view(B, bl, nkv, hd)
|
| 1011 |
+
v = qkv[:, :, qd + kvd:].view(B, bl, nkv, hd)
|
| 1012 |
+
|
| 1013 |
+
qs = q.reshape(B * bl, nh, hd)
|
| 1014 |
+
ks = k.reshape(B * bl, nkv, hd)
|
| 1015 |
+
vs = v.reshape(B * bl, nkv, hd)
|
| 1016 |
+
|
| 1017 |
+
if self._use_rope_inplace:
|
| 1018 |
+
_fi_apply_rope(
|
| 1019 |
+
qs, ks, self._rope_indptr, self._rope_offset,
|
| 1020 |
+
interleave=False, rope_scale=1.0,
|
| 1021 |
+
rope_theta=self._rope_theta,
|
| 1022 |
+
)
|
| 1023 |
+
else:
|
| 1024 |
+
if isinstance(cache_starts, int):
|
| 1025 |
+
cos = self._rope_cos[
|
| 1026 |
+
:, cache_starts: cache_starts + bl, :
|
| 1027 |
+
]
|
| 1028 |
+
sin = self._rope_sin[
|
| 1029 |
+
:, cache_starts: cache_starts + bl, :
|
| 1030 |
+
]
|
| 1031 |
+
else:
|
| 1032 |
+
cos = torch.cat([
|
| 1033 |
+
self._rope_cos[:, cs: cs + bl, :]
|
| 1034 |
+
for cs in cache_starts
|
| 1035 |
+
], dim=0)
|
| 1036 |
+
sin = torch.cat([
|
| 1037 |
+
self._rope_sin[:, cs: cs + bl, :]
|
| 1038 |
+
for cs in cache_starts
|
| 1039 |
+
], dim=0)
|
| 1040 |
+
q, k = self._apply_rotary_pos_emb(
|
| 1041 |
+
q, k, cos, sin, unsqueeze_dim=2,
|
| 1042 |
+
)
|
| 1043 |
+
qs = q.reshape(B * bl, nh, hd)
|
| 1044 |
+
ks = k.reshape(B * bl, nkv, hd)
|
| 1045 |
+
|
| 1046 |
+
self._paged_kv[li][pw_idx, 0, pw_off] = ks
|
| 1047 |
+
self._paged_kv[li][pw_idx, 1, pw_off] = vs
|
| 1048 |
+
|
| 1049 |
+
attn_out = self._fi_eager_wrapper.run(
|
| 1050 |
+
qs, self._paged_kv[li],
|
| 1051 |
+
)
|
| 1052 |
+
attn_out = attn_out.reshape(B, bl, -1)
|
| 1053 |
+
attn_out = self._project("o", li, attn_out)
|
| 1054 |
+
|
| 1055 |
+
hidden = residual + attn_out
|
| 1056 |
+
residual = hidden
|
| 1057 |
+
hidden = self._post_ln[li](hidden)
|
| 1058 |
+
hidden = self._mlp_forward(li, hidden)
|
| 1059 |
+
hidden = residual + hidden
|
| 1060 |
+
|
| 1061 |
+
return self._final_norm(hidden)
|
| 1062 |
+
|
| 1063 |
+
def _block_forward_fused(
|
| 1064 |
+
self,
|
| 1065 |
+
block_emb: Tensor,
|
| 1066 |
+
cache_starts: int | List[int],
|
| 1067 |
+
B: int,
|
| 1068 |
+
bl: int,
|
| 1069 |
+
pw_idx: Tensor,
|
| 1070 |
+
pw_off: Tensor,
|
| 1071 |
+
nh: int,
|
| 1072 |
+
nkv: int,
|
| 1073 |
+
hd: int,
|
| 1074 |
+
qd: int,
|
| 1075 |
+
kvd: int,
|
| 1076 |
+
) -> Tensor:
|
| 1077 |
+
"""``block_forward`` body with fused_add_rmsnorm + silu_and_mul.
|
| 1078 |
+
|
| 1079 |
+
Carries ``(hidden, residual)`` as 2D ``(B*bl, dim)``. ``residual`` is
|
| 1080 |
+
a private clone of ``block_emb`` so the caller's tensor is not mutated.
|
| 1081 |
+
"""
|
| 1082 |
+
L = self._num_layers
|
| 1083 |
+
N = B * bl
|
| 1084 |
+
|
| 1085 |
+
residual = block_emb.reshape(N, -1).contiguous().clone()
|
| 1086 |
+
hidden = self._fi_rmsnorm(
|
| 1087 |
+
residual, self._input_ln_w[0], self._input_ln_eps[0],
|
| 1088 |
+
)
|
| 1089 |
+
|
| 1090 |
+
for li in range(L):
|
| 1091 |
+
qkv = self._project("qkv", li, hidden)
|
| 1092 |
+
qs = qkv[:, :qd].view(N, nh, hd)
|
| 1093 |
+
ks = qkv[:, qd: qd + kvd].view(N, nkv, hd)
|
| 1094 |
+
vs = qkv[:, qd + kvd:].view(N, nkv, hd)
|
| 1095 |
+
|
| 1096 |
+
if self._use_rope_inplace:
|
| 1097 |
+
_fi_apply_rope(
|
| 1098 |
+
qs, ks, self._rope_indptr, self._rope_offset,
|
| 1099 |
+
interleave=False, rope_scale=1.0,
|
| 1100 |
+
rope_theta=self._rope_theta,
|
| 1101 |
+
)
|
| 1102 |
+
else:
|
| 1103 |
+
if isinstance(cache_starts, int):
|
| 1104 |
+
cos = self._rope_cos[
|
| 1105 |
+
:, cache_starts: cache_starts + bl, :
|
| 1106 |
+
]
|
| 1107 |
+
sin = self._rope_sin[
|
| 1108 |
+
:, cache_starts: cache_starts + bl, :
|
| 1109 |
+
]
|
| 1110 |
+
else:
|
| 1111 |
+
cos = torch.cat([
|
| 1112 |
+
self._rope_cos[:, cs: cs + bl, :]
|
| 1113 |
+
for cs in cache_starts
|
| 1114 |
+
], dim=0)
|
| 1115 |
+
sin = torch.cat([
|
| 1116 |
+
self._rope_sin[:, cs: cs + bl, :]
|
| 1117 |
+
for cs in cache_starts
|
| 1118 |
+
], dim=0)
|
| 1119 |
+
q3 = qs.view(B, bl, nh, hd)
|
| 1120 |
+
k3 = ks.view(B, bl, nkv, hd)
|
| 1121 |
+
q3, k3 = self._apply_rotary_pos_emb(
|
| 1122 |
+
q3, k3, cos, sin, unsqueeze_dim=2,
|
| 1123 |
+
)
|
| 1124 |
+
qs = q3.reshape(N, nh, hd)
|
| 1125 |
+
ks = k3.reshape(N, nkv, hd)
|
| 1126 |
+
|
| 1127 |
+
self._paged_kv[li][pw_idx, 0, pw_off] = ks
|
| 1128 |
+
self._paged_kv[li][pw_idx, 1, pw_off] = vs
|
| 1129 |
+
|
| 1130 |
+
attn_out = self._fi_eager_wrapper.run(
|
| 1131 |
+
qs, self._paged_kv[li],
|
| 1132 |
+
)
|
| 1133 |
+
attn_out_2d = attn_out.reshape(N, -1)
|
| 1134 |
+
attn_out_2d = self._project("o", li, attn_out_2d).contiguous()
|
| 1135 |
+
|
| 1136 |
+
self._fi_fused_add_rmsnorm(
|
| 1137 |
+
attn_out_2d, residual,
|
| 1138 |
+
self._post_ln_w[li], self._post_ln_eps[li],
|
| 1139 |
+
)
|
| 1140 |
+
|
| 1141 |
+
mlp_out = self._mlp_forward(li, attn_out_2d)
|
| 1142 |
+
if not mlp_out.is_contiguous():
|
| 1143 |
+
mlp_out = mlp_out.contiguous()
|
| 1144 |
+
|
| 1145 |
+
if li + 1 < L:
|
| 1146 |
+
self._fi_fused_add_rmsnorm(
|
| 1147 |
+
mlp_out, residual,
|
| 1148 |
+
self._input_ln_w[li + 1], self._input_ln_eps[li + 1],
|
| 1149 |
+
)
|
| 1150 |
+
hidden = mlp_out
|
| 1151 |
+
else:
|
| 1152 |
+
residual.add_(mlp_out)
|
| 1153 |
+
hidden = residual
|
| 1154 |
+
|
| 1155 |
+
out = self._fi_rmsnorm(
|
| 1156 |
+
hidden, self._final_norm_w, self._final_norm_eps,
|
| 1157 |
+
)
|
| 1158 |
+
return out.view(B, bl, -1)
|
| 1159 |
+
|
| 1160 |
+
# ------------------------------------------------------------------ #
|
| 1161 |
+
# Cache management
|
| 1162 |
+
# ------------------------------------------------------------------ #
|
| 1163 |
+
|
| 1164 |
+
@property
|
| 1165 |
+
def batch_size(self) -> int:
|
| 1166 |
+
return self._batch_size
|
| 1167 |
+
|
| 1168 |
+
@property
|
| 1169 |
+
def cache_len(self) -> int:
|
| 1170 |
+
"""Backward-compat: cache length of sample 0."""
|
| 1171 |
+
return self._cache_lens[0]
|
| 1172 |
+
|
| 1173 |
+
@cache_len.setter
|
| 1174 |
+
def cache_len(self, value: int):
|
| 1175 |
+
self._cache_lens = [value] * self._batch_size
|
| 1176 |
+
|
| 1177 |
+
@property
|
| 1178 |
+
def cache_lens(self) -> List[int]:
|
| 1179 |
+
return self._cache_lens
|
| 1180 |
+
|
| 1181 |
+
@property
|
| 1182 |
+
def prefix_lens(self) -> List[int]:
|
| 1183 |
+
return self._prefix_lens
|
| 1184 |
+
|
| 1185 |
+
def advance_cache(self, block_len: int):
|
| 1186 |
+
for bi in range(self._batch_size):
|
| 1187 |
+
self._cache_lens[bi] += block_len
|
| 1188 |
+
|
| 1189 |
+
def reset(self):
|
| 1190 |
+
B = self._batch_size
|
| 1191 |
+
self._cache_lens = [0] * B
|
| 1192 |
+
self._prefix_lens = [0] * B
|
| 1193 |
+
self._last_plan_cache_start = -1
|
| 1194 |
+
# CUDA graph / pool are kept across generate() calls. Per-call prefix and
|
| 1195 |
+
# paged-KV state are rebuilt in prefix_forward; block_forward_graph replans
|
| 1196 |
+
# when cache_starts change. capture_cuda_graph early-returns when the same
|
| 1197 |
+
# block_size is already captured (avoids allocator issues + recapture cost).
|
| 1198 |
+
|
| 1199 |
+
def snapshot_kv(self):
|
| 1200 |
+
"""Deep copy of paged KV + lengths for :meth:`restore_kv` (CFG uncond pass)."""
|
| 1201 |
+
kv_copy = [t.clone() for t in self._paged_kv]
|
| 1202 |
+
return (
|
| 1203 |
+
kv_copy,
|
| 1204 |
+
list(self._cache_lens),
|
| 1205 |
+
list(self._prefix_lens),
|
| 1206 |
+
int(getattr(self, "_last_plan_cache_start", -1)),
|
| 1207 |
+
)
|
| 1208 |
+
|
| 1209 |
+
def restore_kv(self, snap) -> None:
|
| 1210 |
+
"""Restore state from :meth:`snapshot_kv`."""
|
| 1211 |
+
kv_copy, cache_lens, prefix_lens, last_plan = snap
|
| 1212 |
+
for li, t in enumerate(kv_copy):
|
| 1213 |
+
self._paged_kv[li].copy_(t)
|
| 1214 |
+
self._cache_lens = cache_lens
|
| 1215 |
+
self._prefix_lens = prefix_lens
|
| 1216 |
+
self._last_plan_cache_start = last_plan
|
| 1217 |
+
|
| 1218 |
+
# ------------------------------------------------------------------ #
|
| 1219 |
+
# CUDA Graph
|
| 1220 |
+
# ------------------------------------------------------------------ #
|
| 1221 |
+
|
| 1222 |
+
@property
|
| 1223 |
+
def has_cuda_graph(self) -> bool:
|
| 1224 |
+
return self._use_cuda_graph
|
| 1225 |
+
|
| 1226 |
+
def can_reuse(
|
| 1227 |
+
self,
|
| 1228 |
+
max_seq_len: int,
|
| 1229 |
+
dtype: torch.dtype,
|
| 1230 |
+
batch_size: int = 1,
|
| 1231 |
+
page_size: int = 16,
|
| 1232 |
+
) -> bool:
|
| 1233 |
+
return (
|
| 1234 |
+
self._max_seq_len >= max_seq_len
|
| 1235 |
+
and self._dtype == dtype
|
| 1236 |
+
and self._batch_size == max(1, batch_size)
|
| 1237 |
+
and self._page_size == page_size
|
| 1238 |
+
)
|
| 1239 |
+
|
| 1240 |
+
def capture_cuda_graph(
|
| 1241 |
+
self,
|
| 1242 |
+
block_size: int,
|
| 1243 |
+
speech_head: "torch.nn.Linear | None" = None,
|
| 1244 |
+
):
|
| 1245 |
+
if self._use_cuda_graph and self._g_block_size == block_size:
|
| 1246 |
+
return
|
| 1247 |
+
|
| 1248 |
+
import gc
|
| 1249 |
+
import os
|
| 1250 |
+
import shutil
|
| 1251 |
+
import glob as glob_mod
|
| 1252 |
+
|
| 1253 |
+
device = self._device
|
| 1254 |
+
dtype = self._dtype
|
| 1255 |
+
bl = block_size
|
| 1256 |
+
B = self._batch_size
|
| 1257 |
+
P = self._pages_per_sample
|
| 1258 |
+
ps = self._page_size
|
| 1259 |
+
hs = self._hidden_size
|
| 1260 |
+
|
| 1261 |
+
cache_root = os.path.expanduser("~/.cache/flashinfer")
|
| 1262 |
+
for partial in glob_mod.glob(
|
| 1263 |
+
os.path.join(cache_root, "**/cached_ops/batch_prefill_*"),
|
| 1264 |
+
recursive=True,
|
| 1265 |
+
):
|
| 1266 |
+
if os.path.isdir(partial):
|
| 1267 |
+
so_files = glob_mod.glob(os.path.join(partial, "*.so"))
|
| 1268 |
+
if not so_files:
|
| 1269 |
+
logger.info(
|
| 1270 |
+
"FlashInferEngine: cleaning partial JIT: %s", partial,
|
| 1271 |
+
)
|
| 1272 |
+
shutil.rmtree(partial, ignore_errors=True)
|
| 1273 |
+
|
| 1274 |
+
# ---- Plan buffers (pre-allocated for CUDA-graph wrapper) ----
|
| 1275 |
+
max_plan_pages = B * P
|
| 1276 |
+
self._plan_qo_indptr = torch.zeros(
|
| 1277 |
+
B + 1, dtype=torch.int32, device=device,
|
| 1278 |
+
)
|
| 1279 |
+
self._plan_paged_kv_indptr = torch.zeros(
|
| 1280 |
+
B + 1, dtype=torch.int32, device=device,
|
| 1281 |
+
)
|
| 1282 |
+
self._plan_paged_kv_indices = torch.zeros(
|
| 1283 |
+
max_plan_pages, dtype=torch.int32, device=device,
|
| 1284 |
+
)
|
| 1285 |
+
self._plan_paged_kv_last_page_len = torch.zeros(
|
| 1286 |
+
B, dtype=torch.int32, device=device,
|
| 1287 |
+
)
|
| 1288 |
+
|
| 1289 |
+
fi_ws = torch.empty(
|
| 1290 |
+
256 * 1024 * 1024, dtype=torch.uint8, device=device,
|
| 1291 |
+
)
|
| 1292 |
+
self._fi_graph_wrapper = flashinfer.BatchPrefillWithPagedKVCacheWrapper(
|
| 1293 |
+
fi_ws,
|
| 1294 |
+
kv_layout="NHD",
|
| 1295 |
+
use_cuda_graph=True,
|
| 1296 |
+
qo_indptr_buf=self._plan_qo_indptr,
|
| 1297 |
+
paged_kv_indptr_buf=self._plan_paged_kv_indptr,
|
| 1298 |
+
paged_kv_indices_buf=self._plan_paged_kv_indices,
|
| 1299 |
+
paged_kv_last_page_len_buf=self._plan_paged_kv_last_page_len,
|
| 1300 |
+
)
|
| 1301 |
+
|
| 1302 |
+
# ---- Graph IO ----
|
| 1303 |
+
self._g_inputs_embeds = torch.zeros(
|
| 1304 |
+
B, bl, hs, device=device, dtype=dtype,
|
| 1305 |
+
)
|
| 1306 |
+
self._g_output_hidden = torch.zeros(
|
| 1307 |
+
B, bl, hs, device=device, dtype=dtype,
|
| 1308 |
+
)
|
| 1309 |
+
self._g_block_size = bl
|
| 1310 |
+
|
| 1311 |
+
uniform_cache = len(set(self._cache_lens)) <= 1
|
| 1312 |
+
if uniform_cache:
|
| 1313 |
+
cs0 = self._cache_lens[0]
|
| 1314 |
+
pw_idx, pw_off = self._compute_page_write_indices(cs0, bl)
|
| 1315 |
+
kv_plan: int | List[int] = cs0 + bl
|
| 1316 |
+
else:
|
| 1317 |
+
starts = list(self._cache_lens)
|
| 1318 |
+
pw_idx, pw_off = self._compute_page_write_indices(starts, bl)
|
| 1319 |
+
kv_plan = [c + bl for c in self._cache_lens]
|
| 1320 |
+
|
| 1321 |
+
self._g_page_write_idx = pw_idx.clone()
|
| 1322 |
+
self._g_page_write_off = pw_off.clone()
|
| 1323 |
+
|
| 1324 |
+
if self._use_rope_inplace:
|
| 1325 |
+
for bi in range(B):
|
| 1326 |
+
self._rope_indptr[bi + 1] = (bi + 1) * bl
|
| 1327 |
+
if uniform_cache:
|
| 1328 |
+
self._rope_offset[:] = self._cache_lens[0]
|
| 1329 |
+
else:
|
| 1330 |
+
for bi in range(B):
|
| 1331 |
+
self._rope_offset[bi] = self._cache_lens[bi]
|
| 1332 |
+
else:
|
| 1333 |
+
self._g_cos = torch.zeros(
|
| 1334 |
+
B, bl, self._head_dim, device=device, dtype=dtype,
|
| 1335 |
+
)
|
| 1336 |
+
self._g_sin = torch.zeros(
|
| 1337 |
+
B, bl, self._head_dim, device=device, dtype=dtype,
|
| 1338 |
+
)
|
| 1339 |
+
for bi in range(B):
|
| 1340 |
+
cs_bi = self._cache_lens[bi]
|
| 1341 |
+
self._g_cos[bi].copy_(self._rope_cos[0, cs_bi: cs_bi + bl, :])
|
| 1342 |
+
self._g_sin[bi].copy_(self._rope_sin[0, cs_bi: cs_bi + bl, :])
|
| 1343 |
+
|
| 1344 |
+
# ---- Speech head in graph ----
|
| 1345 |
+
if speech_head is not None:
|
| 1346 |
+
self._speech_head_w = speech_head.weight.detach()
|
| 1347 |
+
self._speech_head_b = (
|
| 1348 |
+
speech_head.bias.detach()
|
| 1349 |
+
if speech_head.bias is not None
|
| 1350 |
+
else None
|
| 1351 |
+
)
|
| 1352 |
+
vocab_size = speech_head.weight.size(0)
|
| 1353 |
+
self._g_shift_ctx = torch.zeros(
|
| 1354 |
+
B, 1, hs, device=device, dtype=dtype,
|
| 1355 |
+
)
|
| 1356 |
+
self._g_logits = torch.zeros(
|
| 1357 |
+
B, bl, vocab_size, device=device, dtype=dtype,
|
| 1358 |
+
)
|
| 1359 |
+
self._has_speech_head = True
|
| 1360 |
+
logger.info(
|
| 1361 |
+
"FlashInferEngine: speech_head in graph (vocab=%d)",
|
| 1362 |
+
vocab_size,
|
| 1363 |
+
)
|
| 1364 |
+
|
| 1365 |
+
# ---- Initial plan ----
|
| 1366 |
+
logger.info(
|
| 1367 |
+
"FlashInferEngine: CUDA graph JIT compiling batch_prefill "
|
| 1368 |
+
"(MAX_JOBS=%s) ...",
|
| 1369 |
+
os.environ.get("MAX_JOBS", "unset"),
|
| 1370 |
+
)
|
| 1371 |
+
self._plan_paged_attention(
|
| 1372 |
+
self._fi_graph_wrapper, kv_plan, seq_len=bl, causal=False,
|
| 1373 |
+
qo_indptr_buf=self._plan_qo_indptr,
|
| 1374 |
+
paged_kv_indptr_buf=self._plan_paged_kv_indptr,
|
| 1375 |
+
paged_kv_indices_buf=self._plan_paged_kv_indices,
|
| 1376 |
+
paged_kv_last_page_len_buf=self._plan_paged_kv_last_page_len,
|
| 1377 |
+
)
|
| 1378 |
+
# Tuple matches :meth:`block_forward_graph` when ``cache_starts`` is a list;
|
| 1379 |
+
# same-text int ``cache_starts`` triggers one replan on first replay (harmless).
|
| 1380 |
+
self._last_plan_cache_start = tuple(self._cache_lens)
|
| 1381 |
+
|
| 1382 |
+
# ---- Warmup ----
|
| 1383 |
+
logger.info("FlashInferEngine: CUDA graph warmup forward ...")
|
| 1384 |
+
self._graph_body()
|
| 1385 |
+
torch.cuda.synchronize()
|
| 1386 |
+
gc.collect()
|
| 1387 |
+
torch.cuda.synchronize()
|
| 1388 |
+
|
| 1389 |
+
# ---- Capture ----
|
| 1390 |
+
logger.info("FlashInferEngine: CUDA graph capturing ...")
|
| 1391 |
+
self._cuda_graph = torch.cuda.CUDAGraph()
|
| 1392 |
+
with torch.cuda.graph(self._cuda_graph, pool=self._graph_pool):
|
| 1393 |
+
self._graph_body()
|
| 1394 |
+
|
| 1395 |
+
if self._graph_pool is None:
|
| 1396 |
+
self._graph_pool = self._cuda_graph.pool()
|
| 1397 |
+
|
| 1398 |
+
self._use_cuda_graph = True
|
| 1399 |
+
logger.info(
|
| 1400 |
+
"FlashInferEngine: CUDA graph captured "
|
| 1401 |
+
"(block_size=%d, batch=%d, speech_head=%s)",
|
| 1402 |
+
bl, B, self._has_speech_head,
|
| 1403 |
+
)
|
| 1404 |
+
|
| 1405 |
+
def _graph_body(self):
|
| 1406 |
+
"""Graph-capturable batched forward for block denoising.
|
| 1407 |
+
|
| 1408 |
+
In fused-norm mode the layer body carries ``(hidden, residual)`` as 2D
|
| 1409 |
+
``(B*bl, dim)`` and uses :func:`flashinfer.norm.fused_add_rmsnorm` for
|
| 1410 |
+
every "residual add + RMSNorm" pair. The persistent
|
| 1411 |
+
``self._g_inputs_embeds`` buffer doubles as the running residual; it is
|
| 1412 |
+
overwritten via ``copy_(block_emb)`` before each replay, so in-place
|
| 1413 |
+
mutation inside the graph is safe.
|
| 1414 |
+
"""
|
| 1415 |
+
B = self._batch_size
|
| 1416 |
+
bl = self._g_block_size
|
| 1417 |
+
nh = self._num_heads
|
| 1418 |
+
nkv = self._num_kv_heads
|
| 1419 |
+
hd = self._head_dim
|
| 1420 |
+
qd = self._q_dim
|
| 1421 |
+
kvd = self._kv_dim
|
| 1422 |
+
use_ri = self._use_rope_inplace
|
| 1423 |
+
L = self._num_layers
|
| 1424 |
+
N = B * bl
|
| 1425 |
+
|
| 1426 |
+
if self._use_fused_norm:
|
| 1427 |
+
# 2D view of the persistent input buffer; in-place mutation OK
|
| 1428 |
+
# because the buffer is reset every replay (see ``block_forward_graph``).
|
| 1429 |
+
residual = self._g_inputs_embeds.view(N, -1)
|
| 1430 |
+
hidden = self._fi_rmsnorm(
|
| 1431 |
+
residual, self._input_ln_w[0], self._input_ln_eps[0],
|
| 1432 |
+
)
|
| 1433 |
+
|
| 1434 |
+
for li in range(L):
|
| 1435 |
+
qkv = self._project("qkv", li, hidden)
|
| 1436 |
+
qs = qkv[:, :qd].view(N, nh, hd)
|
| 1437 |
+
ks = qkv[:, qd: qd + kvd].view(N, nkv, hd)
|
| 1438 |
+
vs = qkv[:, qd + kvd:].view(N, nkv, hd)
|
| 1439 |
+
|
| 1440 |
+
if use_ri:
|
| 1441 |
+
_fi_apply_rope(
|
| 1442 |
+
qs, ks, self._rope_indptr, self._rope_offset,
|
| 1443 |
+
interleave=False, rope_scale=1.0,
|
| 1444 |
+
rope_theta=self._rope_theta,
|
| 1445 |
+
)
|
| 1446 |
+
else:
|
| 1447 |
+
q3 = qs.view(B, bl, nh, hd)
|
| 1448 |
+
k3 = ks.view(B, bl, nkv, hd)
|
| 1449 |
+
q3, k3 = self._apply_rotary_pos_emb(
|
| 1450 |
+
q3, k3, self._g_cos, self._g_sin, unsqueeze_dim=2,
|
| 1451 |
+
)
|
| 1452 |
+
qs = q3.reshape(N, nh, hd)
|
| 1453 |
+
ks = k3.reshape(N, nkv, hd)
|
| 1454 |
+
|
| 1455 |
+
self._paged_kv[li][
|
| 1456 |
+
self._g_page_write_idx, 0, self._g_page_write_off
|
| 1457 |
+
] = ks
|
| 1458 |
+
self._paged_kv[li][
|
| 1459 |
+
self._g_page_write_idx, 1, self._g_page_write_off
|
| 1460 |
+
] = vs
|
| 1461 |
+
|
| 1462 |
+
attn_out = self._fi_graph_wrapper.run(
|
| 1463 |
+
qs, self._paged_kv[li],
|
| 1464 |
+
)
|
| 1465 |
+
attn_out_2d = attn_out.reshape(N, -1)
|
| 1466 |
+
attn_out_2d = self._project(
|
| 1467 |
+
"o", li, attn_out_2d,
|
| 1468 |
+
).contiguous()
|
| 1469 |
+
|
| 1470 |
+
self._fi_fused_add_rmsnorm(
|
| 1471 |
+
attn_out_2d, residual,
|
| 1472 |
+
self._post_ln_w[li], self._post_ln_eps[li],
|
| 1473 |
+
)
|
| 1474 |
+
|
| 1475 |
+
mlp_out = self._mlp_forward(li, attn_out_2d)
|
| 1476 |
+
if not mlp_out.is_contiguous():
|
| 1477 |
+
mlp_out = mlp_out.contiguous()
|
| 1478 |
+
|
| 1479 |
+
if li + 1 < L:
|
| 1480 |
+
self._fi_fused_add_rmsnorm(
|
| 1481 |
+
mlp_out, residual,
|
| 1482 |
+
self._input_ln_w[li + 1], self._input_ln_eps[li + 1],
|
| 1483 |
+
)
|
| 1484 |
+
hidden = mlp_out
|
| 1485 |
+
else:
|
| 1486 |
+
residual.add_(mlp_out)
|
| 1487 |
+
hidden = residual
|
| 1488 |
+
|
| 1489 |
+
out = self._fi_rmsnorm(
|
| 1490 |
+
hidden, self._final_norm_w, self._final_norm_eps,
|
| 1491 |
+
)
|
| 1492 |
+
self._g_output_hidden[:] = out.view(B, bl, -1)
|
| 1493 |
+
else:
|
| 1494 |
+
hidden = self._g_inputs_embeds
|
| 1495 |
+
|
| 1496 |
+
for li in range(L):
|
| 1497 |
+
residual = hidden
|
| 1498 |
+
hidden = self._input_ln[li](hidden)
|
| 1499 |
+
|
| 1500 |
+
qkv = self._project("qkv", li, hidden)
|
| 1501 |
+
q = qkv[:, :, :qd].view(B, bl, nh, hd)
|
| 1502 |
+
k = qkv[:, :, qd: qd + kvd].view(B, bl, nkv, hd)
|
| 1503 |
+
v = qkv[:, :, qd + kvd:].view(B, bl, nkv, hd)
|
| 1504 |
+
|
| 1505 |
+
qs = q.reshape(B * bl, nh, hd)
|
| 1506 |
+
ks = k.reshape(B * bl, nkv, hd)
|
| 1507 |
+
vs = v.reshape(B * bl, nkv, hd)
|
| 1508 |
+
|
| 1509 |
+
if use_ri:
|
| 1510 |
+
_fi_apply_rope(
|
| 1511 |
+
qs, ks, self._rope_indptr, self._rope_offset,
|
| 1512 |
+
interleave=False, rope_scale=1.0,
|
| 1513 |
+
rope_theta=self._rope_theta,
|
| 1514 |
+
)
|
| 1515 |
+
else:
|
| 1516 |
+
cos = self._g_cos
|
| 1517 |
+
sin = self._g_sin
|
| 1518 |
+
q, k = self._apply_rotary_pos_emb(
|
| 1519 |
+
q, k, cos, sin, unsqueeze_dim=2,
|
| 1520 |
+
)
|
| 1521 |
+
qs = q.reshape(B * bl, nh, hd)
|
| 1522 |
+
ks = k.reshape(B * bl, nkv, hd)
|
| 1523 |
+
|
| 1524 |
+
self._paged_kv[li][
|
| 1525 |
+
self._g_page_write_idx, 0, self._g_page_write_off
|
| 1526 |
+
] = ks
|
| 1527 |
+
self._paged_kv[li][
|
| 1528 |
+
self._g_page_write_idx, 1, self._g_page_write_off
|
| 1529 |
+
] = vs
|
| 1530 |
+
|
| 1531 |
+
attn_out = self._fi_graph_wrapper.run(
|
| 1532 |
+
qs, self._paged_kv[li],
|
| 1533 |
+
)
|
| 1534 |
+
attn_out = attn_out.reshape(B, bl, -1)
|
| 1535 |
+
attn_out = self._project("o", li, attn_out)
|
| 1536 |
+
|
| 1537 |
+
hidden = residual + attn_out
|
| 1538 |
+
residual = hidden
|
| 1539 |
+
hidden = self._post_ln[li](hidden)
|
| 1540 |
+
hidden = self._mlp_forward(li, hidden)
|
| 1541 |
+
hidden = residual + hidden
|
| 1542 |
+
|
| 1543 |
+
self._g_output_hidden[:] = self._final_norm(hidden)
|
| 1544 |
+
|
| 1545 |
+
if self._has_speech_head:
|
| 1546 |
+
shift_hidden = torch.cat(
|
| 1547 |
+
[self._g_shift_ctx, self._g_output_hidden[:, : bl - 1]],
|
| 1548 |
+
dim=1,
|
| 1549 |
+
)
|
| 1550 |
+
self._g_logits[:] = F.linear(
|
| 1551 |
+
shift_hidden,
|
| 1552 |
+
self._speech_head_w,
|
| 1553 |
+
self._speech_head_b,
|
| 1554 |
+
)
|
| 1555 |
+
|
| 1556 |
+
def set_shift_ctx(self, ctx: Tensor):
|
| 1557 |
+
"""Copy shift context into graph buffer (once per block).
|
| 1558 |
+
|
| 1559 |
+
Args:
|
| 1560 |
+
ctx: ``(B, 1, dim)`` or ``(1, 1, dim)`` (broadcast).
|
| 1561 |
+
"""
|
| 1562 |
+
if self._g_shift_ctx is not None:
|
| 1563 |
+
if ctx.size(0) < self._batch_size:
|
| 1564 |
+
self._g_shift_ctx[:] = ctx.expand(
|
| 1565 |
+
self._batch_size, -1, -1,
|
| 1566 |
+
)
|
| 1567 |
+
else:
|
| 1568 |
+
self._g_shift_ctx.copy_(ctx)
|
| 1569 |
+
|
| 1570 |
+
def block_forward_graph(
|
| 1571 |
+
self,
|
| 1572 |
+
block_emb: Tensor,
|
| 1573 |
+
cache_starts: int | List[int],
|
| 1574 |
+
) -> tuple[Tensor, Tensor | None]:
|
| 1575 |
+
"""Batched block forward via CUDA graph replay.
|
| 1576 |
+
|
| 1577 |
+
Falls back to eager :meth:`block_forward` for partial blocks.
|
| 1578 |
+
|
| 1579 |
+
Args:
|
| 1580 |
+
block_emb: ``(B, bl, dim)``
|
| 1581 |
+
cache_starts: int (uniform) or list[int] per-sample.
|
| 1582 |
+
|
| 1583 |
+
Returns:
|
| 1584 |
+
``(hidden, logits)`` — ``(B, bl, dim)`` and
|
| 1585 |
+
``(B, bl, vocab)`` or ``None``.
|
| 1586 |
+
"""
|
| 1587 |
+
bl = block_emb.size(1)
|
| 1588 |
+
if bl != self._g_block_size:
|
| 1589 |
+
return self.block_forward(block_emb, cache_starts), None
|
| 1590 |
+
|
| 1591 |
+
self._g_inputs_embeds.copy_(block_emb)
|
| 1592 |
+
|
| 1593 |
+
cs_key: int | tuple
|
| 1594 |
+
if isinstance(cache_starts, int):
|
| 1595 |
+
cs_key = cache_starts
|
| 1596 |
+
else:
|
| 1597 |
+
cs_key = tuple(cache_starts)
|
| 1598 |
+
|
| 1599 |
+
if cs_key != self._last_plan_cache_start:
|
| 1600 |
+
B = self._batch_size
|
| 1601 |
+
bl_ = bl
|
| 1602 |
+
|
| 1603 |
+
pw_idx, pw_off = self._compute_page_write_indices(
|
| 1604 |
+
cache_starts, bl_,
|
| 1605 |
+
)
|
| 1606 |
+
self._g_page_write_idx.copy_(pw_idx)
|
| 1607 |
+
self._g_page_write_off.copy_(pw_off)
|
| 1608 |
+
|
| 1609 |
+
if self._use_rope_inplace:
|
| 1610 |
+
for bi in range(B):
|
| 1611 |
+
self._rope_indptr[bi + 1] = (bi + 1) * bl_
|
| 1612 |
+
if isinstance(cache_starts, int):
|
| 1613 |
+
self._rope_offset[:] = cache_starts
|
| 1614 |
+
else:
|
| 1615 |
+
for bi in range(B):
|
| 1616 |
+
self._rope_offset[bi] = cache_starts[bi]
|
| 1617 |
+
else:
|
| 1618 |
+
if isinstance(cache_starts, int):
|
| 1619 |
+
for bi in range(B):
|
| 1620 |
+
self._g_cos[bi].copy_(
|
| 1621 |
+
self._rope_cos[0, cache_starts: cache_starts + bl_, :],
|
| 1622 |
+
)
|
| 1623 |
+
self._g_sin[bi].copy_(
|
| 1624 |
+
self._rope_sin[0, cache_starts: cache_starts + bl_, :],
|
| 1625 |
+
)
|
| 1626 |
+
else:
|
| 1627 |
+
for bi in range(B):
|
| 1628 |
+
csb = cache_starts[bi]
|
| 1629 |
+
self._g_cos[bi].copy_(
|
| 1630 |
+
self._rope_cos[0, csb: csb + bl_, :],
|
| 1631 |
+
)
|
| 1632 |
+
self._g_sin[bi].copy_(
|
| 1633 |
+
self._rope_sin[0, csb: csb + bl_, :],
|
| 1634 |
+
)
|
| 1635 |
+
|
| 1636 |
+
if isinstance(cache_starts, int):
|
| 1637 |
+
kv_lens: int | List[int] = cache_starts + bl_
|
| 1638 |
+
else:
|
| 1639 |
+
kv_lens = [c + bl_ for c in cache_starts]
|
| 1640 |
+
|
| 1641 |
+
self._plan_paged_attention(
|
| 1642 |
+
self._fi_graph_wrapper, kv_lens, seq_len=bl_,
|
| 1643 |
+
causal=False,
|
| 1644 |
+
qo_indptr_buf=self._plan_qo_indptr,
|
| 1645 |
+
paged_kv_indptr_buf=self._plan_paged_kv_indptr,
|
| 1646 |
+
paged_kv_indices_buf=self._plan_paged_kv_indices,
|
| 1647 |
+
paged_kv_last_page_len_buf=self._plan_paged_kv_last_page_len,
|
| 1648 |
+
)
|
| 1649 |
+
self._last_plan_cache_start = cs_key
|
| 1650 |
+
|
| 1651 |
+
self._cuda_graph.replay()
|
| 1652 |
+
|
| 1653 |
+
logits = (
|
| 1654 |
+
self._g_logits if self._has_speech_head else None
|
| 1655 |
+
)
|
| 1656 |
+
return self._g_output_hidden, logits
|
runtime/chatterbox-flash/chatterbox_flash/engines/mlx.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MLX (Apple Silicon) inference engine for Chatterbox-Flash.
|
| 2 |
+
|
| 3 |
+
Runs the LLaMA backbone of the T3 decoder natively on Metal via the
|
| 4 |
+
`mlx <https://github.com/ml-explore/mlx>`_ framework, while keeping the
|
| 5 |
+
small auxiliary modules (speech / text / cond embeddings, speech head,
|
| 6 |
+
conditioning encoder) on PyTorch. The PyTorch ↔ MLX boundary is at
|
| 7 |
+
the engine surface, so the block-diffusion ``generate()`` loop does not
|
| 8 |
+
need to know about MLX at all.
|
| 9 |
+
|
| 10 |
+
Memory layout
|
| 11 |
+
-------------
|
| 12 |
+
* T3 conditioning encoder + speech_emb + text_emb + speech_head live
|
| 13 |
+
on the PyTorch device (CPU on a Mac; the host model is loaded with
|
| 14 |
+
``device='cpu'`` for the MLX backend).
|
| 15 |
+
* The LLaMA stack (embed-free, 30 layers for Llama_520M) is rebuilt in
|
| 16 |
+
MLX, with weights converted once from the PyTorch ``state_dict``.
|
| 17 |
+
* A per-layer KV cache lives entirely inside MLX.
|
| 18 |
+
|
| 19 |
+
Tentative-then-commit pattern
|
| 20 |
+
-----------------------------
|
| 21 |
+
The block-diffusion loop calls :meth:`block_forward` multiple times for
|
| 22 |
+
the same block (one per inner-loop step), each time overwriting the
|
| 23 |
+
previous step's tentative KV. We implement this by tracking a
|
| 24 |
+
*committed* length per layer and cropping the cache back to that length
|
| 25 |
+
at the start of each block forward. :meth:`advance_cache` bumps the
|
| 26 |
+
committed length so the next block sees the just-written entries as
|
| 27 |
+
real prefix.
|
| 28 |
+
|
| 29 |
+
Status
|
| 30 |
+
------
|
| 31 |
+
Experimental. Tested with mlx ≥ 0.13 and mlx-lm ≥ 0.10. This module
|
| 32 |
+
imports MLX lazily; the rest of the package works without MLX installed.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
from __future__ import annotations
|
| 36 |
+
|
| 37 |
+
import importlib.util
|
| 38 |
+
import logging
|
| 39 |
+
from typing import List, Optional, Tuple, Union
|
| 40 |
+
|
| 41 |
+
import numpy as np
|
| 42 |
+
import torch
|
| 43 |
+
from torch import Tensor
|
| 44 |
+
|
| 45 |
+
logger = logging.getLogger(__name__)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
MLX_AVAILABLE: bool = (
|
| 49 |
+
importlib.util.find_spec("mlx") is not None
|
| 50 |
+
and importlib.util.find_spec("mlx_lm") is not None
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _require_mlx() -> tuple["module", "module"]: # type: ignore[name-defined]
|
| 55 |
+
if not MLX_AVAILABLE:
|
| 56 |
+
raise ImportError(
|
| 57 |
+
"MLX backend requires the 'mlx' and 'mlx-lm' packages.\n"
|
| 58 |
+
" Install (Apple Silicon Mac only):\n"
|
| 59 |
+
" pip install mlx mlx-lm\n"
|
| 60 |
+
" or:\n"
|
| 61 |
+
" pip install 'chatterbox-flash[mlx]'",
|
| 62 |
+
)
|
| 63 |
+
import mlx.core as mx
|
| 64 |
+
import mlx_lm # noqa: F401 (force import, validate version)
|
| 65 |
+
return mx, mlx_lm
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# --------------------------------------------------------------------------- #
|
| 69 |
+
# Weight conversion: PyTorch HF LlamaModel state_dict → MLX state_dict
|
| 70 |
+
# --------------------------------------------------------------------------- #
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _torch_to_mx(t: torch.Tensor, mx_module): # noqa: ANN001
|
| 74 |
+
"""Convert a torch tensor to an mx.array via numpy (zero extra copy)."""
|
| 75 |
+
arr = t.detach().to(torch.float32).cpu().numpy()
|
| 76 |
+
return mx_module.array(arr)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _convert_llama_weights(
|
| 80 |
+
tfmr_state: dict[str, torch.Tensor],
|
| 81 |
+
mx_module,
|
| 82 |
+
) -> dict[str, "mx_module.array"]: # type: ignore[name-defined]
|
| 83 |
+
"""Convert HF LlamaModel state_dict into an MLX-flat dict.
|
| 84 |
+
|
| 85 |
+
Both PyTorch and mlx-lm store nn.Linear as ``(out, in)`` so no
|
| 86 |
+
transposition is needed. RoPE inv_freq / cos_cached / sin_cached
|
| 87 |
+
buffers are recomputed inside MLX and therefore skipped.
|
| 88 |
+
"""
|
| 89 |
+
SKIP_SUBSTRINGS = (
|
| 90 |
+
"rotary_emb.inv_freq",
|
| 91 |
+
"rotary_emb.cos_cached",
|
| 92 |
+
"rotary_emb.sin_cached",
|
| 93 |
+
)
|
| 94 |
+
out: dict[str, object] = {}
|
| 95 |
+
for k, v in tfmr_state.items():
|
| 96 |
+
if any(s in k for s in SKIP_SUBSTRINGS):
|
| 97 |
+
continue
|
| 98 |
+
out[k] = _torch_to_mx(v, mx_module)
|
| 99 |
+
return out
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# --------------------------------------------------------------------------- #
|
| 103 |
+
# Embed-free MLX LlamaModel wrapper
|
| 104 |
+
# --------------------------------------------------------------------------- #
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class _EmbedFreeLlama:
|
| 108 |
+
"""Thin wrapper around mlx-lm's LlamaModel that accepts inputs_embeds.
|
| 109 |
+
|
| 110 |
+
Reuses the entire mlx-lm LLaMA layer stack (incl. RoPE LLaMA3 scaling
|
| 111 |
+
that ships with mlx-lm) but bypasses the input ``embed_tokens`` layer
|
| 112 |
+
— we already have the speech / text / cond embeddings on the
|
| 113 |
+
PyTorch side.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
tfmr_state: dict[str, torch.Tensor],
|
| 119 |
+
hf_config, # noqa: ANN001
|
| 120 |
+
rotary_emb=None, # noqa: ANN001
|
| 121 |
+
) -> None:
|
| 122 |
+
mx, _ = _require_mlx()
|
| 123 |
+
from mlx_lm.models.llama import LlamaModel, ModelArgs
|
| 124 |
+
|
| 125 |
+
self._mx = mx
|
| 126 |
+
args = ModelArgs(
|
| 127 |
+
model_type="llama",
|
| 128 |
+
hidden_size=int(hf_config.hidden_size),
|
| 129 |
+
num_hidden_layers=int(hf_config.num_hidden_layers),
|
| 130 |
+
intermediate_size=int(hf_config.intermediate_size),
|
| 131 |
+
num_attention_heads=int(hf_config.num_attention_heads),
|
| 132 |
+
num_key_value_heads=int(hf_config.num_key_value_heads),
|
| 133 |
+
rms_norm_eps=float(hf_config.rms_norm_eps),
|
| 134 |
+
vocab_size=int(hf_config.vocab_size),
|
| 135 |
+
head_dim=int(getattr(hf_config, "head_dim", 0))
|
| 136 |
+
or int(hf_config.hidden_size // hf_config.num_attention_heads),
|
| 137 |
+
max_position_embeddings=int(
|
| 138 |
+
getattr(hf_config, "max_position_embeddings", 4096)
|
| 139 |
+
),
|
| 140 |
+
rope_theta=float(getattr(hf_config, "rope_theta", 10000.0)),
|
| 141 |
+
rope_traditional=False,
|
| 142 |
+
rope_scaling=dict(hf_config.rope_scaling)
|
| 143 |
+
if getattr(hf_config, "rope_scaling", None) is not None
|
| 144 |
+
else None,
|
| 145 |
+
tie_word_embeddings=bool(hf_config.tie_word_embeddings),
|
| 146 |
+
)
|
| 147 |
+
self._args = args
|
| 148 |
+
self._llama = LlamaModel(args)
|
| 149 |
+
|
| 150 |
+
# Convert weights (skip rotary buffers; mlx-lm rebuilds them).
|
| 151 |
+
mlx_state = _convert_llama_weights(tfmr_state, mx)
|
| 152 |
+
from mlx.utils import tree_unflatten
|
| 153 |
+
self._llama.load_weights(list(mlx_state.items()), strict=False)
|
| 154 |
+
|
| 155 |
+
# Override RoPE with HF's *exact* inv_freq. Reconstructing it from the
|
| 156 |
+
# config (rope_theta / rope_scaling) is fragile: across transformers
|
| 157 |
+
# versions ``config.rope_theta`` may be absent, in which case the
|
| 158 |
+
# ModelArgs above silently falls back to base=10000 while HF actually
|
| 159 |
+
# used e.g. 500000 — giving a position-dependent RoPE mismatch that
|
| 160 |
+
# scrambles generation. HF's ``rotary_emb.inv_freq`` already bakes in
|
| 161 |
+
# the base *and* any (llama3 / yarn / …) scaling, so using it verbatim
|
| 162 |
+
# is correct regardless of config layout or scaling type.
|
| 163 |
+
self._override_rope_from_hf(rotary_emb)
|
| 164 |
+
|
| 165 |
+
# Optional weight-only quantization of the backbone (4- / 8-bit).
|
| 166 |
+
# Driven by an env var so the tts/model signatures stay untouched
|
| 167 |
+
# (same convention as CHATTERBOX_FLASH_ENGINE). Must run AFTER
|
| 168 |
+
# load_weights so the fp weights are present to quantize, and only
|
| 169 |
+
# touches this LLaMA stack — S3Gen / VoiceEncoder live on the torch
|
| 170 |
+
# side and are unaffected.
|
| 171 |
+
self._quant_bits = self._maybe_quantize()
|
| 172 |
+
|
| 173 |
+
# Force materialization to catch missing keys early.
|
| 174 |
+
mx.eval(self._llama.parameters())
|
| 175 |
+
self._cache_cls = None
|
| 176 |
+
self._init_cache_cls()
|
| 177 |
+
|
| 178 |
+
def _override_rope_from_hf(self, rotary_emb) -> None: # noqa: ANN001
|
| 179 |
+
"""Replace each layer's RoPE with one driven by HF's exact inv_freq."""
|
| 180 |
+
mx = self._mx
|
| 181 |
+
if rotary_emb is None or not hasattr(rotary_emb, "inv_freq"):
|
| 182 |
+
logger.warning(
|
| 183 |
+
"MLX backbone: HF rotary_emb/inv_freq unavailable; falling back "
|
| 184 |
+
"to config-reconstructed RoPE (may mismatch if config lacks "
|
| 185 |
+
"rope_theta).",
|
| 186 |
+
)
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
import numpy as _np
|
| 190 |
+
import mlx.nn as nn
|
| 191 |
+
|
| 192 |
+
inv = rotary_emb.inv_freq.detach().to(torch.float32).cpu().numpy()
|
| 193 |
+
# mx.fast.rope takes per-pair ``freqs`` = wavelengths = 1 / inv_freq.
|
| 194 |
+
freqs = mx.array((1.0 / inv).astype(_np.float32))
|
| 195 |
+
att_scale = float(getattr(rotary_emb, "attention_scaling", 1.0) or 1.0)
|
| 196 |
+
dims = int(self._args.head_dim)
|
| 197 |
+
traditional = bool(self._args.rope_traditional)
|
| 198 |
+
|
| 199 |
+
class _HFRoPE(nn.Module):
|
| 200 |
+
def __init__(self) -> None:
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.dims = dims
|
| 203 |
+
self._freqs = freqs
|
| 204 |
+
self.scale = att_scale
|
| 205 |
+
self.traditional = traditional
|
| 206 |
+
|
| 207 |
+
def __call__(self, x, offset=0): # noqa: ANN001
|
| 208 |
+
y = mx.fast.rope(
|
| 209 |
+
x, self.dims, traditional=self.traditional,
|
| 210 |
+
base=None, scale=1.0, offset=offset, freqs=self._freqs,
|
| 211 |
+
)
|
| 212 |
+
return y if self.scale == 1.0 else y * self.scale
|
| 213 |
+
|
| 214 |
+
for layer in self._llama.layers:
|
| 215 |
+
layer.self_attn.rope = _HFRoPE()
|
| 216 |
+
logger.info(
|
| 217 |
+
"MLX backbone RoPE overridden from HF inv_freq "
|
| 218 |
+
"(dims=%d, attention_scaling=%.4f).", dims, att_scale,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
def _maybe_quantize(self) -> Optional[int]:
|
| 222 |
+
import os
|
| 223 |
+
|
| 224 |
+
raw = os.environ.get("CHATTERBOX_FLASH_MLX_QUANT_BITS", "").strip()
|
| 225 |
+
if not raw:
|
| 226 |
+
return None
|
| 227 |
+
try:
|
| 228 |
+
bits = int(raw)
|
| 229 |
+
except ValueError:
|
| 230 |
+
logger.warning("Ignoring invalid CHATTERBOX_FLASH_MLX_QUANT_BITS=%r", raw)
|
| 231 |
+
return None
|
| 232 |
+
if bits not in (2, 3, 4, 6, 8):
|
| 233 |
+
logger.warning("Unsupported MLX quant bits=%d; skipping.", bits)
|
| 234 |
+
return None
|
| 235 |
+
group_size = int(os.environ.get("CHATTERBOX_FLASH_MLX_QUANT_GROUP", "64"))
|
| 236 |
+
|
| 237 |
+
import mlx.nn as nn
|
| 238 |
+
|
| 239 |
+
# Skip layers whose dims aren't divisible by group_size — mlx requires
|
| 240 |
+
# in_features % group_size == 0, and the speech/text heads live on the
|
| 241 |
+
# torch side anyway, so any odd-shaped Linear here is left in fp.
|
| 242 |
+
def _can_quant(_path: str, module) -> bool: # noqa: ANN001
|
| 243 |
+
if not hasattr(module, "to_quantized"):
|
| 244 |
+
return False
|
| 245 |
+
w = getattr(module, "weight", None)
|
| 246 |
+
return w is not None and w.ndim == 2 and (w.shape[-1] % group_size == 0)
|
| 247 |
+
|
| 248 |
+
nn.quantize(
|
| 249 |
+
self._llama, group_size=group_size, bits=bits,
|
| 250 |
+
class_predicate=_can_quant,
|
| 251 |
+
)
|
| 252 |
+
logger.info(
|
| 253 |
+
"MLX backbone quantized to %d-bit (group_size=%d).", bits, group_size,
|
| 254 |
+
)
|
| 255 |
+
return bits
|
| 256 |
+
|
| 257 |
+
def _init_cache_cls(self) -> None:
|
| 258 |
+
try:
|
| 259 |
+
from mlx_lm.models.cache import KVCache
|
| 260 |
+
self._cache_cls = KVCache
|
| 261 |
+
except ImportError: # very old mlx-lm
|
| 262 |
+
from mlx_lm.models.base import KVCache # type: ignore
|
| 263 |
+
self._cache_cls = KVCache
|
| 264 |
+
|
| 265 |
+
def make_cache(self) -> list:
|
| 266 |
+
return [self._cache_cls() for _ in range(self._args.num_hidden_layers)]
|
| 267 |
+
|
| 268 |
+
def forward(
|
| 269 |
+
self,
|
| 270 |
+
inputs_embeds_mx,
|
| 271 |
+
cache: list,
|
| 272 |
+
*,
|
| 273 |
+
mask=None,
|
| 274 |
+
):
|
| 275 |
+
"""Run the LLaMA stack on continuous embeddings, return hidden_states."""
|
| 276 |
+
mx = self._mx
|
| 277 |
+
x = inputs_embeds_mx
|
| 278 |
+
# Manually mirror mlx-lm LlamaModel.__call__ but without embed_tokens.
|
| 279 |
+
for layer, c in zip(self._llama.layers, cache):
|
| 280 |
+
x = layer(x, mask=mask, cache=c)
|
| 281 |
+
x = self._llama.norm(x)
|
| 282 |
+
return x
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
# --------------------------------------------------------------------------- #
|
| 286 |
+
# Engine
|
| 287 |
+
# --------------------------------------------------------------------------- #
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
CacheStarts = Union[int, List[int]]
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
class MLXEngine:
|
| 294 |
+
"""Apple-Silicon MLX engine. Implements the chatterbox_flash engine protocol."""
|
| 295 |
+
|
| 296 |
+
has_cuda_graph: bool = False # MLX has no CUDA-graph concept
|
| 297 |
+
|
| 298 |
+
def __init__(
|
| 299 |
+
self,
|
| 300 |
+
model, # ChatterboxFlashT3
|
| 301 |
+
max_seq_len: int,
|
| 302 |
+
dtype: torch.dtype,
|
| 303 |
+
*,
|
| 304 |
+
batch_size: int = 1,
|
| 305 |
+
page_size: int = 16, # unused; protocol parity
|
| 306 |
+
) -> None:
|
| 307 |
+
self._model = model
|
| 308 |
+
self._max_seq_len = int(max_seq_len)
|
| 309 |
+
self._dtype = dtype
|
| 310 |
+
self._batch_size = max(1, int(batch_size))
|
| 311 |
+
self._device = model.device
|
| 312 |
+
|
| 313 |
+
mx, _ = _require_mlx()
|
| 314 |
+
self._mx = mx
|
| 315 |
+
if dtype == torch.bfloat16:
|
| 316 |
+
self._mx_dtype = mx.bfloat16
|
| 317 |
+
elif dtype == torch.float16:
|
| 318 |
+
self._mx_dtype = mx.float16
|
| 319 |
+
else:
|
| 320 |
+
self._mx_dtype = mx.float32
|
| 321 |
+
|
| 322 |
+
# Build the MLX backbone exactly once per engine instance.
|
| 323 |
+
tfmr_state = {k: v for k, v in model.tfmr.state_dict().items()}
|
| 324 |
+
self._backbone = _EmbedFreeLlama(
|
| 325 |
+
tfmr_state, model.tfmr.config,
|
| 326 |
+
rotary_emb=getattr(model.tfmr, "rotary_emb", None),
|
| 327 |
+
)
|
| 328 |
+
self._cache = self._backbone.make_cache()
|
| 329 |
+
|
| 330 |
+
# Per-row batch-1 caches, populated only on the ragged (cross-text /
|
| 331 |
+
# per-row prefix length) path. A single dense batch-B cache cannot
|
| 332 |
+
# represent rows with differing sequence lengths, so the ragged path
|
| 333 |
+
# serializes over these instead of slicing ``self._cache`` live.
|
| 334 |
+
self._row_caches: Optional[List[list]] = None
|
| 335 |
+
|
| 336 |
+
# Per-row bookkeeping. MLX's KV cache is shared across the batch
|
| 337 |
+
# dim (same as the SDPA path) so this is uniform across rows.
|
| 338 |
+
self._cache_lens: List[int] = [0] * self._batch_size
|
| 339 |
+
self._prefix_lens: List[int] = [0] * self._batch_size
|
| 340 |
+
|
| 341 |
+
# ------------------------------------------------------------------ #
|
| 342 |
+
# Introspection / lifecycle
|
| 343 |
+
# ------------------------------------------------------------------ #
|
| 344 |
+
|
| 345 |
+
@property
|
| 346 |
+
def batch_size(self) -> int:
|
| 347 |
+
return self._batch_size
|
| 348 |
+
|
| 349 |
+
@property
|
| 350 |
+
def cache_len(self) -> int:
|
| 351 |
+
return self._cache_lens[0]
|
| 352 |
+
|
| 353 |
+
@cache_len.setter
|
| 354 |
+
def cache_len(self, value: int) -> None:
|
| 355 |
+
self._cache_lens = [int(value)] * self._batch_size
|
| 356 |
+
|
| 357 |
+
@property
|
| 358 |
+
def cache_lens(self) -> List[int]:
|
| 359 |
+
return self._cache_lens
|
| 360 |
+
|
| 361 |
+
@property
|
| 362 |
+
def prefix_lens(self) -> List[int]:
|
| 363 |
+
return self._prefix_lens
|
| 364 |
+
|
| 365 |
+
def reset(self) -> None:
|
| 366 |
+
self._cache = self._backbone.make_cache()
|
| 367 |
+
self._row_caches = None
|
| 368 |
+
self._cache_lens = [0] * self._batch_size
|
| 369 |
+
self._prefix_lens = [0] * self._batch_size
|
| 370 |
+
|
| 371 |
+
def can_reuse(
|
| 372 |
+
self,
|
| 373 |
+
max_seq_len: int,
|
| 374 |
+
dtype: torch.dtype,
|
| 375 |
+
*,
|
| 376 |
+
batch_size: int = 1,
|
| 377 |
+
page_size: int = 16,
|
| 378 |
+
) -> bool:
|
| 379 |
+
return (
|
| 380 |
+
self._max_seq_len >= max_seq_len
|
| 381 |
+
and self._dtype == dtype
|
| 382 |
+
and self._batch_size == max(1, batch_size)
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
# ------------------------------------------------------------------ #
|
| 386 |
+
# Tensor boundary
|
| 387 |
+
# ------------------------------------------------------------------ #
|
| 388 |
+
|
| 389 |
+
def _torch_to_mx(self, t: Tensor):
|
| 390 |
+
"""torch (cpu / mps / cuda) → mx.array (Metal). Routes via numpy on host."""
|
| 391 |
+
return self._mx.array(t.detach().to(torch.float32).cpu().numpy()).astype(
|
| 392 |
+
self._mx_dtype,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
def _mx_to_torch(self, a) -> Tensor:
|
| 396 |
+
"""mx.array → torch on the host device, restoring our dtype."""
|
| 397 |
+
# mx.array → numpy → torch (always lands on the model's device).
|
| 398 |
+
arr = np.array(a.astype(self._mx.float32))
|
| 399 |
+
return torch.from_numpy(arr).to(device=self._device, dtype=self._dtype)
|
| 400 |
+
|
| 401 |
+
# ------------------------------------------------------------------ #
|
| 402 |
+
# KV-cache cropping (tentative-then-commit pattern)
|
| 403 |
+
# ------------------------------------------------------------------ #
|
| 404 |
+
|
| 405 |
+
def _crop_cache(self, cache: list, length: int) -> None:
|
| 406 |
+
"""Truncate every layer of ``cache`` to ``length`` time steps."""
|
| 407 |
+
mx = self._mx
|
| 408 |
+
for c in cache:
|
| 409 |
+
# mlx-lm's KVCache exposes ``keys`` / ``values`` (shape
|
| 410 |
+
# ``(B, n_kv_h, T, head_dim)``) and an ``offset`` int.
|
| 411 |
+
if c.keys is None:
|
| 412 |
+
continue
|
| 413 |
+
c.keys = c.keys[..., :length, :]
|
| 414 |
+
c.values = c.values[..., :length, :]
|
| 415 |
+
c.offset = int(length)
|
| 416 |
+
mx.eval(*[(c.keys, c.values) for c in cache if c.keys is not None])
|
| 417 |
+
|
| 418 |
+
def _crop_cache_to(self, length: int) -> None:
|
| 419 |
+
"""Truncate the shared (uniform-path) KV cache to ``length`` steps."""
|
| 420 |
+
self._crop_cache(self._cache, length)
|
| 421 |
+
|
| 422 |
+
# ------------------------------------------------------------------ #
|
| 423 |
+
# Prefix forward
|
| 424 |
+
# ------------------------------------------------------------------ #
|
| 425 |
+
|
| 426 |
+
def prefix_forward(
|
| 427 |
+
self,
|
| 428 |
+
prefix_emb_or_list: Union[Tensor, List[Tensor]],
|
| 429 |
+
) -> Tensor:
|
| 430 |
+
"""Run the cond/text/SOS prefix once and seed the cache."""
|
| 431 |
+
if isinstance(prefix_emb_or_list, list):
|
| 432 |
+
return self._prefix_forward_ragged(prefix_emb_or_list)
|
| 433 |
+
|
| 434 |
+
prefix_emb = prefix_emb_or_list
|
| 435 |
+
B_pfx = prefix_emb.size(0)
|
| 436 |
+
if B_pfx not in (1, self._batch_size):
|
| 437 |
+
raise ValueError(
|
| 438 |
+
f"MLX prefix_forward: prefix batch {B_pfx} must be 1 or "
|
| 439 |
+
f"engine batch {self._batch_size}",
|
| 440 |
+
)
|
| 441 |
+
if B_pfx == 1 and self._batch_size > 1:
|
| 442 |
+
prefix_emb = prefix_emb.expand(self._batch_size, -1, -1).contiguous()
|
| 443 |
+
|
| 444 |
+
lp = int(prefix_emb.size(1))
|
| 445 |
+
# MLX expects a 3D input (B, T, D).
|
| 446 |
+
x_mx = self._torch_to_mx(prefix_emb)
|
| 447 |
+
# The conditioning prefix must be processed CAUSALLY (matching the HF
|
| 448 |
+
# SDPA path, which gets an implicit causal mask). _EmbedFreeLlama
|
| 449 |
+
# bypasses LlamaModel.__call__, so mask=None would leave the prefix
|
| 450 |
+
# fully bidirectional and corrupt the cached context. Use mlx's native
|
| 451 |
+
# "causal" string mask — exactly what mlx-lm passes for a causal
|
| 452 |
+
# forward — so it is dtype-safe regardless of the backbone weight dtype.
|
| 453 |
+
out_mx = self._backbone.forward(x_mx, self._cache, mask="causal")
|
| 454 |
+
self._mx.eval(out_mx)
|
| 455 |
+
|
| 456 |
+
for bi in range(self._batch_size):
|
| 457 |
+
self._cache_lens[bi] = lp
|
| 458 |
+
self._prefix_lens[bi] = lp
|
| 459 |
+
|
| 460 |
+
# SOS is at index lp - 1.
|
| 461 |
+
shift_ctx = self._mx_to_torch(out_mx[:, lp - 1 : lp, :])
|
| 462 |
+
return shift_ctx.contiguous()
|
| 463 |
+
|
| 464 |
+
def _prefix_forward_ragged(self, emb_list: List[Tensor]) -> Tensor:
|
| 465 |
+
"""Pad-and-mask path for per-row prefix lengths (cross-text / null prefix)."""
|
| 466 |
+
if len(emb_list) == 1:
|
| 467 |
+
return self.prefix_forward(emb_list[0])
|
| 468 |
+
|
| 469 |
+
mx = self._mx
|
| 470 |
+
B = len(emb_list)
|
| 471 |
+
if B != self._batch_size:
|
| 472 |
+
raise ValueError(
|
| 473 |
+
f"MLX ragged prefix: list length {B} != engine batch "
|
| 474 |
+
f"{self._batch_size}",
|
| 475 |
+
)
|
| 476 |
+
lp_list = [int(e.size(1)) for e in emb_list]
|
| 477 |
+
lp_max = max(lp_list)
|
| 478 |
+
dim = emb_list[0].size(-1)
|
| 479 |
+
|
| 480 |
+
padded = torch.zeros(B, lp_max, dim, dtype=self._dtype, device=self._device)
|
| 481 |
+
for bi, e in enumerate(emb_list):
|
| 482 |
+
padded[bi, : e.size(1)] = e.squeeze(0).to(self._dtype)
|
| 483 |
+
|
| 484 |
+
# Additive padding mask, combined with causal triangular mask.
|
| 485 |
+
x_mx = self._torch_to_mx(padded)
|
| 486 |
+
neg_inf = mx.array(-1e9, dtype=self._mx_dtype)
|
| 487 |
+
pad_mask = mx.zeros((B, lp_max), dtype=self._mx_dtype)
|
| 488 |
+
for bi, lp_i in enumerate(lp_list):
|
| 489 |
+
if lp_i < lp_max:
|
| 490 |
+
pad_mask[bi, lp_i:] = neg_inf
|
| 491 |
+
causal = mx.triu(
|
| 492 |
+
mx.full((lp_max, lp_max), neg_inf, dtype=self._mx_dtype),
|
| 493 |
+
k=1,
|
| 494 |
+
)
|
| 495 |
+
attn_mask = pad_mask[:, None, None, :] + causal[None, None]
|
| 496 |
+
out_mx = self._backbone.forward(x_mx, self._cache, mask=attn_mask)
|
| 497 |
+
mx.eval(out_mx)
|
| 498 |
+
|
| 499 |
+
for bi, lp_i in enumerate(lp_list):
|
| 500 |
+
self._cache_lens[bi] = lp_i
|
| 501 |
+
self._prefix_lens[bi] = lp_i
|
| 502 |
+
|
| 503 |
+
# Split the padded batch-B prefix cache into per-row batch-1 caches,
|
| 504 |
+
# each trimmed to that row's *valid* prefix length (the padded tail
|
| 505 |
+
# carries masked garbage and must be dropped). The ragged
|
| 506 |
+
# ``block_forward`` serializes over these so a batch-1 block input
|
| 507 |
+
# never gets concatenated against batch-B cache keys.
|
| 508 |
+
row_caches: List[list] = []
|
| 509 |
+
for bi, lp_i in enumerate(lp_list):
|
| 510 |
+
rc = self._backbone.make_cache()
|
| 511 |
+
for src, dst in zip(self._cache, rc):
|
| 512 |
+
if src.keys is None:
|
| 513 |
+
continue
|
| 514 |
+
dst.keys = src.keys[bi : bi + 1, :, :lp_i, :]
|
| 515 |
+
dst.values = src.values[bi : bi + 1, :, :lp_i, :]
|
| 516 |
+
dst.offset = int(lp_i)
|
| 517 |
+
mx.eval(*[(c.keys, c.values) for c in rc if c.keys is not None])
|
| 518 |
+
row_caches.append(rc)
|
| 519 |
+
self._row_caches = row_caches
|
| 520 |
+
|
| 521 |
+
# SOS row index per sample (lp_i - 1). Gather on the torch side.
|
| 522 |
+
hidden = self._mx_to_torch(out_mx)
|
| 523 |
+
idx = torch.tensor(
|
| 524 |
+
[lp_i - 1 for lp_i in lp_list], device=self._device, dtype=torch.long,
|
| 525 |
+
)
|
| 526 |
+
shift_ctx = hidden.gather(
|
| 527 |
+
1, idx[:, None, None].expand(B, 1, hidden.size(-1)),
|
| 528 |
+
)
|
| 529 |
+
return shift_ctx.contiguous()
|
| 530 |
+
|
| 531 |
+
# ------------------------------------------------------------------ #
|
| 532 |
+
# Block forward
|
| 533 |
+
# ------------------------------------------------------------------ #
|
| 534 |
+
|
| 535 |
+
def block_forward(
|
| 536 |
+
self,
|
| 537 |
+
block_emb: Tensor,
|
| 538 |
+
cache_starts: CacheStarts,
|
| 539 |
+
) -> Tensor:
|
| 540 |
+
"""Tentative block forward; KV is written, advance_cache commits it."""
|
| 541 |
+
B = int(block_emb.size(0))
|
| 542 |
+
bl = int(block_emb.size(1))
|
| 543 |
+
|
| 544 |
+
if isinstance(cache_starts, int):
|
| 545 |
+
cs_uniform: Optional[int] = cache_starts
|
| 546 |
+
else:
|
| 547 |
+
cs_list = list(cache_starts)
|
| 548 |
+
if len(set(cs_list)) == 1:
|
| 549 |
+
cs_uniform = cs_list[0]
|
| 550 |
+
else:
|
| 551 |
+
cs_uniform = None
|
| 552 |
+
|
| 553 |
+
if cs_uniform is not None:
|
| 554 |
+
self._crop_cache_to(int(cs_uniform))
|
| 555 |
+
x_mx = self._torch_to_mx(block_emb)
|
| 556 |
+
# mlx-lm builds a causal mask internally when T > 1 and the
|
| 557 |
+
# cache has an offset; the block sees the cropped prefix +
|
| 558 |
+
# itself. No explicit mask needed here.
|
| 559 |
+
out_mx = self._backbone.forward(x_mx, self._cache)
|
| 560 |
+
self._mx.eval(out_mx)
|
| 561 |
+
return self._mx_to_torch(out_mx)
|
| 562 |
+
|
| 563 |
+
# Ragged: per-row cache_starts differ (cross-text / null-prefix).
|
| 564 |
+
# mlx-lm exposes no per-row position offset, and a dense batch-B
|
| 565 |
+
# cache cannot hold rows of differing length, so we serialize over
|
| 566 |
+
# the per-row batch-1 caches seeded at prefix time. Each row's
|
| 567 |
+
# cache is cropped to that row's committed length (cs_i), the block
|
| 568 |
+
# is forwarded tentatively, and the write persists in the row cache
|
| 569 |
+
# for the next step / commit — mirroring the uniform path per row.
|
| 570 |
+
# We deliberately serialize to keep correctness > throughput.
|
| 571 |
+
mx = self._mx
|
| 572 |
+
if self._row_caches is None:
|
| 573 |
+
raise RuntimeError(
|
| 574 |
+
"MLX ragged block_forward requires per-row caches; "
|
| 575 |
+
"prefix_forward must run with a per-row prefix list first.",
|
| 576 |
+
)
|
| 577 |
+
outs = []
|
| 578 |
+
for bi, cs_i in enumerate(cs_list): # type: ignore[name-defined]
|
| 579 |
+
rc = self._row_caches[bi]
|
| 580 |
+
self._crop_cache(rc, int(cs_i))
|
| 581 |
+
x_mx_i = self._torch_to_mx(block_emb[bi : bi + 1])
|
| 582 |
+
out_mx_i = self._backbone.forward(x_mx_i, rc)
|
| 583 |
+
mx.eval(out_mx_i)
|
| 584 |
+
outs.append(self._mx_to_torch(out_mx_i))
|
| 585 |
+
return torch.cat(outs, dim=0)
|
| 586 |
+
|
| 587 |
+
def block_forward_graph(
|
| 588 |
+
self,
|
| 589 |
+
block_emb: Tensor,
|
| 590 |
+
cache_starts: CacheStarts,
|
| 591 |
+
) -> Tuple[Tensor, Optional[Tensor]]:
|
| 592 |
+
"""No graph capture on MLX → delegate to eager."""
|
| 593 |
+
return self.block_forward(block_emb, cache_starts), None
|
| 594 |
+
|
| 595 |
+
def advance_cache(self, block_len: int) -> None:
|
| 596 |
+
"""Commit ``block_len`` newly-written entries."""
|
| 597 |
+
for bi in range(self._batch_size):
|
| 598 |
+
self._cache_lens[bi] += int(block_len)
|
| 599 |
+
|
| 600 |
+
def set_shift_ctx(self, ctx: Tensor) -> None: # noqa: D401
|
| 601 |
+
"""No graph buffer to stage; the caller folds shift_ctx in-line."""
|
| 602 |
+
return None
|
| 603 |
+
|
| 604 |
+
def capture_cuda_graph(
|
| 605 |
+
self,
|
| 606 |
+
block_size: int,
|
| 607 |
+
*,
|
| 608 |
+
speech_head: Optional[torch.nn.Linear] = None,
|
| 609 |
+
) -> None: # noqa: D401
|
| 610 |
+
"""MLX has no CUDA-graph concept; intentionally a no-op."""
|
| 611 |
+
logger.debug(
|
| 612 |
+
"MLXEngine.capture_cuda_graph(block_size=%d) is a no-op.",
|
| 613 |
+
block_size,
|
| 614 |
+
)
|
| 615 |
+
return None
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
__all__ = [
|
| 619 |
+
"MLXEngine",
|
| 620 |
+
"MLX_AVAILABLE",
|
| 621 |
+
]
|
runtime/chatterbox-flash/chatterbox_flash/engines/torch_sdpa.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure-PyTorch fallback inference engine for Chatterbox-Flash.
|
| 2 |
+
|
| 3 |
+
Implements the same engine protocol as :class:`FlashInferEngine` but on
|
| 4 |
+
top of the stock HuggingFace transformers ``LlamaModel`` SDPA backend.
|
| 5 |
+
No CUDA-graph capture, no paged buffer, no flashinfer kernels — just
|
| 6 |
+
``transformers.DynamicCache`` with the standard causal/bidirectional
|
| 7 |
+
attention masks.
|
| 8 |
+
|
| 9 |
+
This is the engine selected automatically when ``flashinfer-python`` is
|
| 10 |
+
not importable, e.g. on Ampere/Turing GPUs or in environments where the
|
| 11 |
+
flashinfer wheels aren't available. It produces numerically matching
|
| 12 |
+
outputs (up to fp32/bf16 reduction-order differences) at lower
|
| 13 |
+
throughput.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import logging
|
| 19 |
+
from typing import List, Optional, Tuple, Union
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
from torch import Tensor
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
CacheStarts = Union[int, List[int]]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _crop_dynamic_cache(cache, max_length: int) -> None:
|
| 32 |
+
"""Truncate a :class:`transformers.DynamicCache` to ``max_length`` tokens.
|
| 33 |
+
|
| 34 |
+
Compatible with both the new ``cache.crop()`` API (transformers ≥ 4.46)
|
| 35 |
+
and the older direct-tensor layout.
|
| 36 |
+
"""
|
| 37 |
+
if max_length is None:
|
| 38 |
+
return
|
| 39 |
+
if hasattr(cache, "crop"):
|
| 40 |
+
cache.crop(max_length)
|
| 41 |
+
else:
|
| 42 |
+
for li in range(len(cache.key_cache)):
|
| 43 |
+
cache.key_cache[li] = cache.key_cache[li][:, :, :max_length, :]
|
| 44 |
+
cache.value_cache[li] = cache.value_cache[li][:, :, :max_length, :]
|
| 45 |
+
cache._seen_tokens = max_length
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TorchSDPAEngine:
|
| 49 |
+
"""Pure-PyTorch SDPA + :class:`DynamicCache` fallback engine."""
|
| 50 |
+
|
| 51 |
+
has_cuda_graph: bool = False
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
model,
|
| 56 |
+
max_seq_len: int,
|
| 57 |
+
dtype: torch.dtype,
|
| 58 |
+
*,
|
| 59 |
+
batch_size: int = 1,
|
| 60 |
+
page_size: int = 16, # ignored; protocol parity
|
| 61 |
+
) -> None:
|
| 62 |
+
from transformers import DynamicCache # local import; lazy
|
| 63 |
+
|
| 64 |
+
self._model = model
|
| 65 |
+
self._max_seq_len = int(max_seq_len)
|
| 66 |
+
self._dtype = dtype
|
| 67 |
+
self._batch_size = max(1, int(batch_size))
|
| 68 |
+
self._device = model.device
|
| 69 |
+
|
| 70 |
+
self._DynamicCache = DynamicCache
|
| 71 |
+
|
| 72 |
+
self._dc = DynamicCache()
|
| 73 |
+
# ``cache_lens[bi]`` is the *committed* (advance_cache-ed) length per
|
| 74 |
+
# sample. In the SDPA path the underlying DynamicCache is uniform
|
| 75 |
+
# across the batch, so the per-sample bookkeeping is mostly for
|
| 76 |
+
# protocol parity with FlashInfer; the per-sample length is read only
|
| 77 |
+
# by capacity checks / introspection.
|
| 78 |
+
self._cache_lens: List[int] = [0] * self._batch_size
|
| 79 |
+
self._prefix_lens: List[int] = [0] * self._batch_size
|
| 80 |
+
|
| 81 |
+
# ------------------------------------------------------------------ #
|
| 82 |
+
# Introspection / lifecycle
|
| 83 |
+
# ------------------------------------------------------------------ #
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def batch_size(self) -> int:
|
| 87 |
+
return self._batch_size
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def cache_len(self) -> int:
|
| 91 |
+
"""Committed cache length (uniform across the batch under SDPA)."""
|
| 92 |
+
return self._cache_lens[0]
|
| 93 |
+
|
| 94 |
+
@cache_len.setter
|
| 95 |
+
def cache_len(self, value: int) -> None:
|
| 96 |
+
self._cache_lens = [int(value)] * self._batch_size
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def cache_lens(self) -> List[int]:
|
| 100 |
+
return self._cache_lens
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def prefix_lens(self) -> List[int]:
|
| 104 |
+
return self._prefix_lens
|
| 105 |
+
|
| 106 |
+
def reset(self) -> None:
|
| 107 |
+
self._dc = self._DynamicCache()
|
| 108 |
+
self._cache_lens = [0] * self._batch_size
|
| 109 |
+
self._prefix_lens = [0] * self._batch_size
|
| 110 |
+
|
| 111 |
+
def can_reuse(
|
| 112 |
+
self,
|
| 113 |
+
max_seq_len: int,
|
| 114 |
+
dtype: torch.dtype,
|
| 115 |
+
*,
|
| 116 |
+
batch_size: int = 1,
|
| 117 |
+
page_size: int = 16,
|
| 118 |
+
) -> bool:
|
| 119 |
+
return (
|
| 120 |
+
self._max_seq_len >= max_seq_len
|
| 121 |
+
and self._dtype == dtype
|
| 122 |
+
and self._batch_size == max(1, batch_size)
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def snapshot_kv(self):
|
| 126 |
+
"""Lightweight snapshot for CFG uncond restore."""
|
| 127 |
+
ks = [t.clone() for t in self._dc.key_cache]
|
| 128 |
+
vs = [t.clone() for t in self._dc.value_cache]
|
| 129 |
+
return (ks, vs, list(self._cache_lens), list(self._prefix_lens))
|
| 130 |
+
|
| 131 |
+
def restore_kv(self, snap) -> None:
|
| 132 |
+
ks, vs, cl, pl = snap
|
| 133 |
+
self._dc = self._DynamicCache()
|
| 134 |
+
for k, v in zip(ks, vs):
|
| 135 |
+
self._dc.key_cache.append(k.clone())
|
| 136 |
+
self._dc.value_cache.append(v.clone())
|
| 137 |
+
self._dc._seen_tokens = ks[0].shape[-2] if ks else 0
|
| 138 |
+
self._cache_lens = list(cl)
|
| 139 |
+
self._prefix_lens = list(pl)
|
| 140 |
+
|
| 141 |
+
# ------------------------------------------------------------------ #
|
| 142 |
+
# Prefix forward
|
| 143 |
+
# ------------------------------------------------------------------ #
|
| 144 |
+
|
| 145 |
+
def prefix_forward(
|
| 146 |
+
self,
|
| 147 |
+
prefix_emb_or_list: Union[Tensor, List[Tensor]],
|
| 148 |
+
) -> Tensor:
|
| 149 |
+
"""Run the cond/text/SOS prefix once and seed the cache."""
|
| 150 |
+
if isinstance(prefix_emb_or_list, list):
|
| 151 |
+
return self._prefix_forward_ragged(prefix_emb_or_list)
|
| 152 |
+
|
| 153 |
+
prefix_emb = prefix_emb_or_list
|
| 154 |
+
B_pfx = prefix_emb.size(0)
|
| 155 |
+
if B_pfx not in (1, self._batch_size):
|
| 156 |
+
raise ValueError(
|
| 157 |
+
f"SDPA prefix_forward: prefix batch {B_pfx} must be 1 or "
|
| 158 |
+
f"engine batch {self._batch_size}",
|
| 159 |
+
)
|
| 160 |
+
if B_pfx == 1 and self._batch_size > 1:
|
| 161 |
+
prefix_emb = prefix_emb.expand(self._batch_size, -1, -1).contiguous()
|
| 162 |
+
|
| 163 |
+
lp = prefix_emb.size(1)
|
| 164 |
+
device = self._device
|
| 165 |
+
pos_ids = (
|
| 166 |
+
torch.arange(lp, device=device)
|
| 167 |
+
.unsqueeze(0)
|
| 168 |
+
.expand(self._batch_size, -1)
|
| 169 |
+
.contiguous()
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
cfg = self._model.tfmr.config
|
| 173 |
+
old_impl = getattr(cfg, "_attn_implementation", "sdpa")
|
| 174 |
+
cfg._attn_implementation = "sdpa"
|
| 175 |
+
try:
|
| 176 |
+
out = self._model.tfmr(
|
| 177 |
+
input_ids=None,
|
| 178 |
+
inputs_embeds=prefix_emb.to(self._dtype),
|
| 179 |
+
position_ids=pos_ids,
|
| 180 |
+
past_key_values=self._dc,
|
| 181 |
+
use_cache=True,
|
| 182 |
+
return_dict=True,
|
| 183 |
+
)
|
| 184 |
+
finally:
|
| 185 |
+
cfg._attn_implementation = old_impl
|
| 186 |
+
|
| 187 |
+
self._dc = out.past_key_values
|
| 188 |
+
for bi in range(self._batch_size):
|
| 189 |
+
self._cache_lens[bi] = lp
|
| 190 |
+
self._prefix_lens[bi] = lp
|
| 191 |
+
|
| 192 |
+
shift_ctx = out.last_hidden_state[:, lp - 1 : lp, :].contiguous()
|
| 193 |
+
return shift_ctx
|
| 194 |
+
|
| 195 |
+
def _prefix_forward_ragged(self, emb_list: List[Tensor]) -> Tensor:
|
| 196 |
+
"""Cross-text path: pad to max length, mask out padding in attention.
|
| 197 |
+
|
| 198 |
+
Each sample gets its own prefix length; the cache is built from a
|
| 199 |
+
left-padded forward. Padding tokens are attention-masked so they
|
| 200 |
+
contribute zero KV, but the cache slots still exist — downstream
|
| 201 |
+
block forwards know to skip them via per-sample ``cache_starts``.
|
| 202 |
+
"""
|
| 203 |
+
if len(emb_list) == 1:
|
| 204 |
+
return self.prefix_forward(emb_list[0])
|
| 205 |
+
|
| 206 |
+
B = len(emb_list)
|
| 207 |
+
if B != self._batch_size:
|
| 208 |
+
raise ValueError(
|
| 209 |
+
f"SDPA ragged prefix: list length {B} != engine batch "
|
| 210 |
+
f"{self._batch_size}",
|
| 211 |
+
)
|
| 212 |
+
lp_list = [int(e.size(1)) for e in emb_list]
|
| 213 |
+
lp_max = max(lp_list)
|
| 214 |
+
dim = emb_list[0].size(-1)
|
| 215 |
+
device = self._device
|
| 216 |
+
|
| 217 |
+
padded = torch.zeros(B, lp_max, dim, device=device, dtype=self._dtype)
|
| 218 |
+
for bi, e in enumerate(emb_list):
|
| 219 |
+
padded[bi, : e.size(1)] = e.to(self._dtype).squeeze(0)
|
| 220 |
+
# Additive mask: 0 for visible, -inf for padding (additive on attn scores).
|
| 221 |
+
# transformers SDPA expects 4D mask (B, 1, Q, K).
|
| 222 |
+
pad_mask = torch.zeros(B, lp_max, device=device, dtype=self._dtype)
|
| 223 |
+
for bi, lp_i in enumerate(lp_list):
|
| 224 |
+
if lp_i < lp_max:
|
| 225 |
+
pad_mask[bi, lp_i:] = float("-inf")
|
| 226 |
+
attn_mask = pad_mask[:, None, None, :].expand(B, 1, lp_max, lp_max)
|
| 227 |
+
# Combine with causal: HF SDPA does causal internally if attention_mask
|
| 228 |
+
# is 2D (B, K); for the 4D additive mask path we have to OR in causal.
|
| 229 |
+
causal = torch.triu(
|
| 230 |
+
torch.full((lp_max, lp_max), float("-inf"), device=device, dtype=self._dtype),
|
| 231 |
+
diagonal=1,
|
| 232 |
+
)
|
| 233 |
+
attn_mask = attn_mask + causal[None, None]
|
| 234 |
+
|
| 235 |
+
pos_ids = (
|
| 236 |
+
torch.arange(lp_max, device=device).unsqueeze(0).expand(B, -1).contiguous()
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
cfg = self._model.tfmr.config
|
| 240 |
+
old_impl = getattr(cfg, "_attn_implementation", "sdpa")
|
| 241 |
+
cfg._attn_implementation = "sdpa"
|
| 242 |
+
try:
|
| 243 |
+
out = self._model.tfmr(
|
| 244 |
+
input_ids=None,
|
| 245 |
+
inputs_embeds=padded,
|
| 246 |
+
position_ids=pos_ids,
|
| 247 |
+
past_key_values=self._dc,
|
| 248 |
+
use_cache=True,
|
| 249 |
+
return_dict=True,
|
| 250 |
+
attention_mask=attn_mask,
|
| 251 |
+
)
|
| 252 |
+
finally:
|
| 253 |
+
cfg._attn_implementation = old_impl
|
| 254 |
+
|
| 255 |
+
self._dc = out.past_key_values
|
| 256 |
+
for bi, lp_i in enumerate(lp_list):
|
| 257 |
+
self._cache_lens[bi] = lp_i
|
| 258 |
+
self._prefix_lens[bi] = lp_i
|
| 259 |
+
|
| 260 |
+
# SOS hidden lives at index lp_i - 1 within each row.
|
| 261 |
+
idx = torch.tensor(
|
| 262 |
+
[lp_i - 1 for lp_i in lp_list], device=device, dtype=torch.long,
|
| 263 |
+
)
|
| 264 |
+
shift_ctx = out.last_hidden_state.gather(
|
| 265 |
+
1,
|
| 266 |
+
idx[:, None, None].expand(B, 1, out.last_hidden_state.size(-1)),
|
| 267 |
+
)
|
| 268 |
+
return shift_ctx.contiguous()
|
| 269 |
+
|
| 270 |
+
# ------------------------------------------------------------------ #
|
| 271 |
+
# Block forward
|
| 272 |
+
# ------------------------------------------------------------------ #
|
| 273 |
+
|
| 274 |
+
def block_forward(
|
| 275 |
+
self,
|
| 276 |
+
block_emb: Tensor,
|
| 277 |
+
cache_starts: CacheStarts,
|
| 278 |
+
) -> Tensor:
|
| 279 |
+
"""Transient block forward; KV is written but not committed.
|
| 280 |
+
|
| 281 |
+
Caller must re-invoke with the same ``cache_starts`` between
|
| 282 |
+
unmasking steps; the cache is re-cropped each time so the new
|
| 283 |
+
block's keys/values overwrite the previous step's tentative KV.
|
| 284 |
+
"""
|
| 285 |
+
B = block_emb.size(0)
|
| 286 |
+
bl = block_emb.size(1)
|
| 287 |
+
device = self._device
|
| 288 |
+
dtype = self._dtype
|
| 289 |
+
|
| 290 |
+
if isinstance(cache_starts, int):
|
| 291 |
+
cs_uniform: Optional[int] = cache_starts
|
| 292 |
+
else:
|
| 293 |
+
cs_list = list(cache_starts)
|
| 294 |
+
if len(set(cs_list)) == 1:
|
| 295 |
+
cs_uniform = cs_list[0]
|
| 296 |
+
else:
|
| 297 |
+
cs_uniform = None
|
| 298 |
+
|
| 299 |
+
if cs_uniform is not None:
|
| 300 |
+
_crop_dynamic_cache(self._dc, cs_uniform)
|
| 301 |
+
pos_ids = (
|
| 302 |
+
torch.arange(cs_uniform, cs_uniform + bl, device=device)
|
| 303 |
+
.unsqueeze(0).expand(B, -1).contiguous()
|
| 304 |
+
)
|
| 305 |
+
kv_len = cs_uniform + bl
|
| 306 |
+
# Full-visible mask within the block (already-causal prefix in cache).
|
| 307 |
+
attn_mask = torch.zeros((1, 1, bl, kv_len), dtype=dtype, device=device)
|
| 308 |
+
else:
|
| 309 |
+
cs_max = max(cs_list)
|
| 310 |
+
_crop_dynamic_cache(self._dc, cs_max)
|
| 311 |
+
# Pad each row's positions to cs_max + bl; mask out the prefix
|
| 312 |
+
# slots beyond cs_i so the block forward only attends to that
|
| 313 |
+
# sample's true prefix.
|
| 314 |
+
pos_rows = torch.zeros(B, bl, device=device, dtype=torch.long)
|
| 315 |
+
for bi, cs in enumerate(cs_list):
|
| 316 |
+
pos_rows[bi] = torch.arange(cs, cs + bl, device=device)
|
| 317 |
+
pos_ids = pos_rows
|
| 318 |
+
kv_len = cs_max + bl
|
| 319 |
+
attn_mask = torch.zeros(B, 1, bl, kv_len, dtype=dtype, device=device)
|
| 320 |
+
for bi, cs in enumerate(cs_list):
|
| 321 |
+
if cs < cs_max:
|
| 322 |
+
attn_mask[bi, 0, :, cs:cs_max] = float("-inf")
|
| 323 |
+
|
| 324 |
+
cfg = self._model.tfmr.config
|
| 325 |
+
old_impl = getattr(cfg, "_attn_implementation", "sdpa")
|
| 326 |
+
cfg._attn_implementation = "sdpa"
|
| 327 |
+
try:
|
| 328 |
+
out = self._model.tfmr(
|
| 329 |
+
input_ids=None,
|
| 330 |
+
inputs_embeds=block_emb.to(dtype),
|
| 331 |
+
position_ids=pos_ids,
|
| 332 |
+
past_key_values=self._dc,
|
| 333 |
+
use_cache=True,
|
| 334 |
+
return_dict=True,
|
| 335 |
+
attention_mask=attn_mask,
|
| 336 |
+
)
|
| 337 |
+
finally:
|
| 338 |
+
cfg._attn_implementation = old_impl
|
| 339 |
+
|
| 340 |
+
self._dc = out.past_key_values
|
| 341 |
+
return out.last_hidden_state
|
| 342 |
+
|
| 343 |
+
def block_forward_graph(
|
| 344 |
+
self,
|
| 345 |
+
block_emb: Tensor,
|
| 346 |
+
cache_starts: CacheStarts,
|
| 347 |
+
) -> Tuple[Tensor, Optional[Tensor]]:
|
| 348 |
+
"""SDPA has no CUDA-graph fast path → delegate to eager."""
|
| 349 |
+
return self.block_forward(block_emb, cache_starts), None
|
| 350 |
+
|
| 351 |
+
def advance_cache(self, block_len: int) -> None:
|
| 352 |
+
"""Commit ``block_len`` newly-written entries; bump per-sample lens."""
|
| 353 |
+
for bi in range(self._batch_size):
|
| 354 |
+
self._cache_lens[bi] += int(block_len)
|
| 355 |
+
|
| 356 |
+
def set_shift_ctx(self, ctx: Tensor) -> None: # noqa: D401
|
| 357 |
+
"""No-op; the SDPA path computes the shift in-line in the caller."""
|
| 358 |
+
return None
|
| 359 |
+
|
| 360 |
+
def capture_cuda_graph(
|
| 361 |
+
self,
|
| 362 |
+
block_size: int,
|
| 363 |
+
*,
|
| 364 |
+
speech_head: Optional[torch.nn.Linear] = None,
|
| 365 |
+
) -> None: # noqa: D401
|
| 366 |
+
"""No CUDA-graph support; intentionally a no-op."""
|
| 367 |
+
logger.debug(
|
| 368 |
+
"TorchSDPAEngine.capture_cuda_graph(block_size=%d) is a no-op "
|
| 369 |
+
"(SDPA backend has no CUDA-graph fast path).",
|
| 370 |
+
block_size,
|
| 371 |
+
)
|
| 372 |
+
return None
|
runtime/chatterbox-flash/chatterbox_flash/model.py
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``ChatterboxFlashT3`` — block-diffusion T3 decoder for Chatterbox-Flash.
|
| 2 |
+
|
| 3 |
+
We extend Chatterbox-TTS's :class:`~chatterbox.models.t3.t3.T3` decoder with a
|
| 4 |
+
single extra ``[MASK]`` token (``id = V``) added to the speech embedding, and
|
| 5 |
+
replace the autoregressive ``inference()`` path with a block-diffusion
|
| 6 |
+
``generate()`` method.
|
| 7 |
+
|
| 8 |
+
The decoder is otherwise unchanged: same Llama backbone, same conditioning
|
| 9 |
+
encoder, same text / speech heads. This keeps the released checkpoint
|
| 10 |
+
compatible with the public ``chatterbox-tts`` weights for everything but the
|
| 11 |
+
expanded ``speech_emb`` (which is initialized by copying the V rows from the
|
| 12 |
+
pretrained checkpoint and adding one normal-initialized row for ``[MASK]``).
|
| 13 |
+
|
| 14 |
+
The ``generate()`` method here implements the production configuration
|
| 15 |
+
described in the paper:
|
| 16 |
+
|
| 17 |
+
* dual-stream-free single-stream KV cache (``[prefix | speech]``);
|
| 18 |
+
* FlashInfer paged KV cache + CUDA-graph capture of the per-block forward;
|
| 19 |
+
* zero-text-batch classifier-free guidance with ``zero_all`` null prefix
|
| 20 |
+
(text and conditioning zeroed, speech-x_t duplicated); the only
|
| 21 |
+
PMI/CFG combination is ``pmi_cfg`` — ``(1+w)·pmi_c − w·pmi_u`` on the
|
| 22 |
+
PMI scale, no other mode exposed;
|
| 23 |
+
* **pmi_count_early** outlier rule — OmniVoice's r_n count schedule
|
| 24 |
+
(controlled by ``omnivoice_schedule_t_shift``) as the per-step
|
| 25 |
+
**floor**, plus the paper's time-shifted PMI-quantile
|
| 26 |
+
``q_k = max(0, 1 - τ · (k+1)/K)`` as an early-decoding **ceiling**.
|
| 27 |
+
When ``time_shift_tau == 0`` the schedule reduces to plain pmi_count
|
| 28 |
+
(fixed-K, no early decoding);
|
| 29 |
+
* prior-calibrated PMI scoring
|
| 30 |
+
``s_i = log p_i(\\hat x_i) - log p̄(\\hat x_i)`` with the **precomputed
|
| 31 |
+
unconditional block prior** ``p̄`` — a forward on
|
| 32 |
+
``[SOS, MASK × block_size]`` with no conditioning, lazy-cached on the
|
| 33 |
+
model object so it's evaluated at most once per
|
| 34 |
+
``(block_size, dtype, device)`` triple over the lifetime of the model;
|
| 35 |
+
* optional Gumbel ``position_temperature`` perturbing the PMI ranking
|
| 36 |
+
for stochastic top-k position selection (OmniVoice §3.4 uses T=5).
|
| 37 |
+
|
| 38 |
+
Training / experimental code paths (full block-diffusion forward, dual-stream,
|
| 39 |
+
speaker probe, magi attention, sdpa/flex inference backends, alternative CFG
|
| 40 |
+
modes and outlier methods, remasking, …) are intentionally omitted.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
from __future__ import annotations
|
| 44 |
+
|
| 45 |
+
import contextlib
|
| 46 |
+
import logging
|
| 47 |
+
import math
|
| 48 |
+
from typing import Literal, Optional
|
| 49 |
+
|
| 50 |
+
import torch
|
| 51 |
+
import torch.nn as nn
|
| 52 |
+
import torch.nn.functional as F
|
| 53 |
+
from torch import Tensor
|
| 54 |
+
|
| 55 |
+
from chatterbox.models.t3.t3 import T3 as _ChatterboxT3
|
| 56 |
+
from chatterbox.models.t3.modules.cond_enc import T3Cond
|
| 57 |
+
from chatterbox.models.t3.modules.t3_config import T3Config
|
| 58 |
+
|
| 59 |
+
from .cfg_guidance import apply_zero_text_cfg_from_logits, pmi_cfg_combine
|
| 60 |
+
from .engines import FLASHINFER_AVAILABLE, InferenceEngine, build_engine
|
| 61 |
+
from .calibration import compute_pmi
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
logger = logging.getLogger(__name__)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# --------------------------------------------------------------------------- #
|
| 68 |
+
# Sampling helpers
|
| 69 |
+
# --------------------------------------------------------------------------- #
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _gumbel_sample(logits: Tensor, temperature: float) -> Tensor:
|
| 73 |
+
scaled = logits / temperature
|
| 74 |
+
u = torch.rand_like(scaled)
|
| 75 |
+
gumbel = -torch.log(-torch.log(u + 1e-10) + 1e-10)
|
| 76 |
+
return scaled + gumbel
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _sample_at_temperature(
|
| 80 |
+
logits: Tensor,
|
| 81 |
+
temperature: float,
|
| 82 |
+
*,
|
| 83 |
+
sampling: Literal["multinomial", "gumbel"] = "multinomial",
|
| 84 |
+
) -> Tensor:
|
| 85 |
+
if sampling == "gumbel":
|
| 86 |
+
return _gumbel_sample(logits, temperature).argmax(dim=-1)
|
| 87 |
+
probs = F.softmax(logits / temperature, dim=-1)
|
| 88 |
+
return torch.multinomial(probs, num_samples=1).squeeze(-1)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# --------------------------------------------------------------------------- #
|
| 92 |
+
# Prefix-shaping helpers (CFG ``zero_all`` null branch)
|
| 93 |
+
# --------------------------------------------------------------------------- #
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _cond_emb_zero_all(ce: Tensor) -> Tensor:
|
| 97 |
+
"""Zero the entire conditioning prefix (paper's ``zero_all`` null branch)."""
|
| 98 |
+
return torch.zeros_like(ce)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _zero_text_content_keep_pad(
|
| 102 |
+
text_emb: Tensor,
|
| 103 |
+
text_tokens: Tensor,
|
| 104 |
+
text_token_lens: Tensor | None,
|
| 105 |
+
) -> Tensor:
|
| 106 |
+
"""Null-text replacement: non-pad text positions go to 0, pad positions copy.
|
| 107 |
+
|
| 108 |
+
Pad positions are detected from ``text_tokens == -100`` and / or
|
| 109 |
+
``text_token_lens`` when supplied. This keeps the sequence length identical
|
| 110 |
+
so the CFG forward can be batched 2x.
|
| 111 |
+
"""
|
| 112 |
+
squeeze_batch = text_tokens.ndim == 1
|
| 113 |
+
toks = text_tokens.unsqueeze(0) if squeeze_batch else text_tokens
|
| 114 |
+
emb = text_emb.unsqueeze(0) if text_emb.ndim == 2 else text_emb
|
| 115 |
+
Btxt, Tt = toks.shape[0], toks.size(1)
|
| 116 |
+
device = toks.device
|
| 117 |
+
is_pad = toks.eq(-100)
|
| 118 |
+
if text_token_lens is not None:
|
| 119 |
+
ttl = text_token_lens.to(device=device, dtype=torch.long).reshape(-1)
|
| 120 |
+
if ttl.numel() == 1 and Btxt > 1:
|
| 121 |
+
ttl = ttl.expand(Btxt)
|
| 122 |
+
is_pad = is_pad | (torch.arange(Tt, device=device)[None, :] >= ttl[:, None])
|
| 123 |
+
out = emb.clone()
|
| 124 |
+
out.masked_fill_((~is_pad).unsqueeze(-1), 0.0)
|
| 125 |
+
return out.squeeze(0) if squeeze_batch else out
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# --------------------------------------------------------------------------- #
|
| 129 |
+
# OmniVoice §3.4 r_n count schedule
|
| 130 |
+
# --------------------------------------------------------------------------- #
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _omnivoice_unmask_schedule(
|
| 134 |
+
n_total_mask: int,
|
| 135 |
+
num_steps: int,
|
| 136 |
+
t_shift: float,
|
| 137 |
+
) -> list[int]:
|
| 138 |
+
"""Per-step unmask **counts** under OmniVoice's time-shifted ``r_n`` curve.
|
| 139 |
+
|
| 140 |
+
Cumulative fraction unmasked by the end of step ``s`` is
|
| 141 |
+
``t_s = r_n(s/K, τ)`` where
|
| 142 |
+
``r_n(s, τ) = τ * s / (1 + (τ - 1) * s)`` (eq. (3) of OmniVoice).
|
| 143 |
+
We then convert the cumulative target into per-step integer counts by
|
| 144 |
+
cumulative rounding (``target = round(N * t_{s+1})``,
|
| 145 |
+
``num_s = max(0, target - cum)``), capped at the remaining masks.
|
| 146 |
+
The last step always closes out whatever is left.
|
| 147 |
+
|
| 148 |
+
Returns a list of length ``num_steps`` summing to ``n_total_mask``.
|
| 149 |
+
"""
|
| 150 |
+
K = max(1, int(num_steps))
|
| 151 |
+
N = max(0, int(n_total_mask))
|
| 152 |
+
if N == 0:
|
| 153 |
+
return [0] * K
|
| 154 |
+
if K == 1:
|
| 155 |
+
return [N]
|
| 156 |
+
tau = float(t_shift)
|
| 157 |
+
if tau <= 0:
|
| 158 |
+
# Degenerate: split as evenly as possible, last step takes remainder.
|
| 159 |
+
base = N // K
|
| 160 |
+
out = [base] * K
|
| 161 |
+
out[-1] += N - sum(out)
|
| 162 |
+
return out
|
| 163 |
+
ts = [tau * (s / K) / (1.0 + (tau - 1.0) * (s / K)) for s in range(K + 1)]
|
| 164 |
+
counts: list[int] = []
|
| 165 |
+
cum = 0
|
| 166 |
+
rem = N
|
| 167 |
+
for s in range(K):
|
| 168 |
+
if s == K - 1:
|
| 169 |
+
num = rem
|
| 170 |
+
else:
|
| 171 |
+
target = int(round(N * ts[s + 1]))
|
| 172 |
+
num = max(0, target - cum)
|
| 173 |
+
num = min(num, rem)
|
| 174 |
+
counts.append(num)
|
| 175 |
+
cum += num
|
| 176 |
+
rem -= num
|
| 177 |
+
return counts
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# --------------------------------------------------------------------------- #
|
| 181 |
+
# Speech-block step (block-diffusion inner loop)
|
| 182 |
+
# --------------------------------------------------------------------------- #
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _pmi_count_early_step_unmask(
|
| 186 |
+
blk_logits_2d: Tensor,
|
| 187 |
+
blk_toks_1d: Tensor,
|
| 188 |
+
*,
|
| 189 |
+
bl: int,
|
| 190 |
+
k: int,
|
| 191 |
+
K: int,
|
| 192 |
+
MASK: int,
|
| 193 |
+
device: torch.device,
|
| 194 |
+
temperature: float,
|
| 195 |
+
temperature_sampling: Literal["multinomial", "gumbel"],
|
| 196 |
+
omnivoice_unmask_schedule_k: int,
|
| 197 |
+
time_shift_tau: float,
|
| 198 |
+
prior_override_v: Tensor | None,
|
| 199 |
+
block_marginal_prior: Tensor | None,
|
| 200 |
+
pmi_cfg_probs_c_bl: Tensor | None,
|
| 201 |
+
pmi_cfg_probs_u_bl: Tensor | None,
|
| 202 |
+
cfg_scale_for_pmi: float,
|
| 203 |
+
position_temperature: float,
|
| 204 |
+
) -> dict:
|
| 205 |
+
"""One denoising step for a single sample block — ``pmi_count_early``.
|
| 206 |
+
|
| 207 |
+
Per-step unmask count combines OmniVoice's count schedule (floor) with
|
| 208 |
+
the paper's PMI-quantile (ceiling):
|
| 209 |
+
|
| 210 |
+
n_sched = omnivoice_unmask_schedule_k (OmniVoice r_n at step k)
|
| 211 |
+
q_k = max(0, 1 - time_shift_tau * (k+1) / K)
|
| 212 |
+
tau_k = Quantile({pmi_i}_{i in M}, q_k) (if time_shift_tau>0)
|
| 213 |
+
n_quant = |{i in M : pmi_i >= tau_k}| (else 0)
|
| 214 |
+
n_unmask = min(max(n_sched, n_quant), m_cur)
|
| 215 |
+
|
| 216 |
+
The top ``n_unmask`` masked positions by PMI get unmasked. Properties:
|
| 217 |
+
* ``time_shift_tau == 0`` reduces to plain pmi_count (fixed-K, no
|
| 218 |
+
early decoding); per-step count is exactly ``n_sched``.
|
| 219 |
+
* ``time_shift_tau > 0`` may unmask more than ``n_sched`` when PMI
|
| 220 |
+
is concentrated, allowing the block to early-finish (n_mask==0)
|
| 221 |
+
before step ``K-1`` — the *only* source of step savings above the
|
| 222 |
+
baseline.
|
| 223 |
+
|
| 224 |
+
PMI scoring:
|
| 225 |
+
* Numerator: probabilities from the conditional softmax of the
|
| 226 |
+
outer block logits (``probs_bl``).
|
| 227 |
+
* Denominator: ``prior_override_v`` (precomputed unconditional
|
| 228 |
+
block prior) when provided, otherwise the running per-block
|
| 229 |
+
marginal frozen at step 0 (``block_marginal_prior``).
|
| 230 |
+
* Under CFG (``cfg_scale > 0``) the caller passes
|
| 231 |
+
``pmi_cfg_probs_{c,u}_bl`` and the CFG-combined PMI
|
| 232 |
+
``(1+w) * pmi_c - w * pmi_u`` is used (this is the only PMI/CFG
|
| 233 |
+
combination supported — no other ``cfg_prior_mode`` is exposed).
|
| 234 |
+
|
| 235 |
+
Gumbel position scoring (OmniVoice §3.4 T=5): when
|
| 236 |
+
``position_temperature > 0`` we add Gumbel noise scaled by ``T`` to
|
| 237 |
+
masked positions' PMI before ranking, turning deterministic top-k
|
| 238 |
+
into stochastic top-k without replacement.
|
| 239 |
+
"""
|
| 240 |
+
probs_raw = F.softmax(blk_logits_2d, dim=-1)
|
| 241 |
+
if temperature > 0:
|
| 242 |
+
sampled = _sample_at_temperature(
|
| 243 |
+
blk_logits_2d, temperature, sampling=temperature_sampling,
|
| 244 |
+
)
|
| 245 |
+
else:
|
| 246 |
+
sampled = blk_logits_2d.argmax(dim=-1)
|
| 247 |
+
|
| 248 |
+
ar = torch.arange(bl, device=device)
|
| 249 |
+
is_mask = blk_toks_1d == MASK
|
| 250 |
+
|
| 251 |
+
# ---- PMI scoring ----
|
| 252 |
+
probs_bl = probs_raw
|
| 253 |
+
if k == 0 and block_marginal_prior is None:
|
| 254 |
+
block_marginal_prior = probs_bl.mean(dim=0)
|
| 255 |
+
|
| 256 |
+
if pmi_cfg_probs_c_bl is not None and pmi_cfg_probs_u_bl is not None:
|
| 257 |
+
mp_c = (
|
| 258 |
+
prior_override_v
|
| 259 |
+
if prior_override_v is not None
|
| 260 |
+
else pmi_cfg_probs_c_bl.mean(dim=0)
|
| 261 |
+
)
|
| 262 |
+
mp_u = (
|
| 263 |
+
prior_override_v
|
| 264 |
+
if prior_override_v is not None
|
| 265 |
+
else pmi_cfg_probs_u_bl.mean(dim=0)
|
| 266 |
+
)
|
| 267 |
+
pmi_c = compute_pmi(pmi_cfg_probs_c_bl, sampled, marginal_prior=mp_c)
|
| 268 |
+
pmi_u = compute_pmi(pmi_cfg_probs_u_bl, sampled, marginal_prior=mp_u)
|
| 269 |
+
pmi = pmi_cfg_combine(pmi_c, pmi_u, cfg_scale_for_pmi)
|
| 270 |
+
else:
|
| 271 |
+
_mp = (
|
| 272 |
+
prior_override_v
|
| 273 |
+
if prior_override_v is not None
|
| 274 |
+
else block_marginal_prior
|
| 275 |
+
)
|
| 276 |
+
pmi = compute_pmi(probs_bl, sampled, marginal_prior=_mp)
|
| 277 |
+
|
| 278 |
+
if position_temperature > 0.0:
|
| 279 |
+
noise = _gumbel_sample(torch.zeros_like(pmi), position_temperature)
|
| 280 |
+
pmi = torch.where(is_mask, pmi + noise, pmi)
|
| 281 |
+
|
| 282 |
+
# ---- Outlier rule (pmi_count_early): max(count-schedule, quantile) ----
|
| 283 |
+
if k == K - 1:
|
| 284 |
+
do_unmask = is_mask
|
| 285 |
+
else:
|
| 286 |
+
masked_idx = ar[is_mask]
|
| 287 |
+
m_cur = int(masked_idx.numel())
|
| 288 |
+
do_unmask = torch.zeros_like(is_mask, dtype=torch.bool)
|
| 289 |
+
if m_cur > 0:
|
| 290 |
+
pmi_m = pmi[masked_idx]
|
| 291 |
+
n_sched = max(0, min(int(omnivoice_unmask_schedule_k), m_cur))
|
| 292 |
+
if time_shift_tau > 0.0:
|
| 293 |
+
q_shifted = max(0.0, 1.0 - time_shift_tau * (k + 1) / K)
|
| 294 |
+
tau_k = torch.quantile(pmi_m.float(), q_shifted).to(dtype=pmi_m.dtype)
|
| 295 |
+
n_quant = int((pmi_m >= tau_k).sum().item())
|
| 296 |
+
else:
|
| 297 |
+
n_quant = 0
|
| 298 |
+
n_unmask = min(max(n_sched, n_quant), m_cur)
|
| 299 |
+
if n_unmask >= m_cur:
|
| 300 |
+
do_unmask[masked_idx] = True
|
| 301 |
+
elif n_unmask > 0:
|
| 302 |
+
_, _topi = pmi_m.topk(n_unmask)
|
| 303 |
+
do_unmask[masked_idx[_topi]] = True
|
| 304 |
+
|
| 305 |
+
xt_step = torch.where(do_unmask, sampled, blk_toks_1d)
|
| 306 |
+
n_mask = int((xt_step == MASK).sum().item())
|
| 307 |
+
return dict(
|
| 308 |
+
xt_step=xt_step,
|
| 309 |
+
n_mask=n_mask,
|
| 310 |
+
block_marginal_prior=block_marginal_prior,
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
# --------------------------------------------------------------------------- #
|
| 315 |
+
# ChatterboxFlashT3
|
| 316 |
+
# --------------------------------------------------------------------------- #
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
class ChatterboxFlashT3(_ChatterboxT3):
|
| 320 |
+
"""Chatterbox-TTS T3 decoder + [MASK] token + block-diffusion generate().
|
| 321 |
+
|
| 322 |
+
The Llama backbone, conditioning encoder, text / speech embeddings and
|
| 323 |
+
heads come straight from the parent ``chatterbox.models.t3.t3.T3``. We
|
| 324 |
+
only add the extra mask-token row in :attr:`speech_emb` and override
|
| 325 |
+
:meth:`generate` with the block-diffusion inference loop.
|
| 326 |
+
"""
|
| 327 |
+
|
| 328 |
+
def __init__(
|
| 329 |
+
self,
|
| 330 |
+
hp: T3Config | None = None,
|
| 331 |
+
*,
|
| 332 |
+
drf_block_size: int = 16,
|
| 333 |
+
) -> None:
|
| 334 |
+
super().__init__(hp=hp)
|
| 335 |
+
if self.is_gpt:
|
| 336 |
+
raise NotImplementedError(
|
| 337 |
+
"ChatterboxFlashT3 currently supports the Llama backbone only "
|
| 338 |
+
"(hp.llama_config_name='Llama_520M').",
|
| 339 |
+
)
|
| 340 |
+
self._mask_token_id: int = self.hp.speech_tokens_dict_size
|
| 341 |
+
self.drf_block_size = int(drf_block_size)
|
| 342 |
+
|
| 343 |
+
# Extend speech_emb by one row for the [MASK] token (id = V).
|
| 344 |
+
old_speech_emb = self.speech_emb
|
| 345 |
+
self.speech_emb = nn.Embedding(self.hp.speech_tokens_dict_size + 1, self.dim)
|
| 346 |
+
with torch.no_grad():
|
| 347 |
+
self.speech_emb.weight[: self.hp.speech_tokens_dict_size].copy_(
|
| 348 |
+
old_speech_emb.weight,
|
| 349 |
+
)
|
| 350 |
+
nn.init.normal_(
|
| 351 |
+
self.speech_emb.weight[self.hp.speech_tokens_dict_size :], std=0.02,
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
# Backward-compat alias so :class:`FlashInferEngine` can read
|
| 355 |
+
# ``model.tfmr.config`` (chatterbox T3 only exposes ``.cfg``).
|
| 356 |
+
self.config = self.cfg
|
| 357 |
+
# Inference engine cache (re-used between calls when shape compatible).
|
| 358 |
+
self._cached_engine: InferenceEngine | None = None
|
| 359 |
+
|
| 360 |
+
# ------------------------------------------------------------------ #
|
| 361 |
+
# Properties / helpers
|
| 362 |
+
# ------------------------------------------------------------------ #
|
| 363 |
+
|
| 364 |
+
@property
|
| 365 |
+
def mask_token_id(self) -> int:
|
| 366 |
+
return self._mask_token_id
|
| 367 |
+
|
| 368 |
+
@property
|
| 369 |
+
def is_llama(self) -> bool:
|
| 370 |
+
return not self.is_gpt
|
| 371 |
+
|
| 372 |
+
def set_block_size(self, block_size: int) -> None:
|
| 373 |
+
self.drf_block_size = int(block_size)
|
| 374 |
+
|
| 375 |
+
def prime_uncond_block_prior(
|
| 376 |
+
self,
|
| 377 |
+
dtype: torch.dtype,
|
| 378 |
+
device: torch.device | str,
|
| 379 |
+
block_size: int | None = None,
|
| 380 |
+
) -> None:
|
| 381 |
+
"""Eagerly compute & cache the PMI unconditional block prior.
|
| 382 |
+
|
| 383 |
+
Called at load time so the first ``generate()`` does not pay for the
|
| 384 |
+
one-shot prior forward — keeping it out of the timed / RTF loop. Uses
|
| 385 |
+
``self.drf_block_size`` unless ``block_size`` is given; a later
|
| 386 |
+
``generate()`` with a different block size recomputes automatically
|
| 387 |
+
(the cache is keyed on ``(block_size, dtype, device)``).
|
| 388 |
+
"""
|
| 389 |
+
device = torch.device(device) if isinstance(device, str) else device
|
| 390 |
+
bs = int(block_size) if block_size is not None else int(self.drf_block_size)
|
| 391 |
+
with torch.no_grad():
|
| 392 |
+
self._compute_uncond_block_prior(
|
| 393 |
+
bs, int(self.hp.start_speech_token), int(self.mask_token_id),
|
| 394 |
+
dtype, device,
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
def _embed_speech_tokens(self, tokens: Tensor) -> Tensor:
|
| 398 |
+
"""Embed speech tokens (including the extra ``[MASK]`` id)."""
|
| 399 |
+
emb = self.speech_emb(tokens)
|
| 400 |
+
if self.hp.input_pos_emb == "learned":
|
| 401 |
+
emb = emb + self.speech_pos_emb(tokens)
|
| 402 |
+
return emb
|
| 403 |
+
|
| 404 |
+
# ------------------------------------------------------------------ #
|
| 405 |
+
# Precomputed unconditional block prior (PMI denominator)
|
| 406 |
+
# ------------------------------------------------------------------ #
|
| 407 |
+
|
| 408 |
+
def _compute_uncond_block_prior(
|
| 409 |
+
self,
|
| 410 |
+
block_size: int,
|
| 411 |
+
SOS: int,
|
| 412 |
+
MASK: int,
|
| 413 |
+
dtype: torch.dtype,
|
| 414 |
+
device: torch.device,
|
| 415 |
+
) -> Tensor:
|
| 416 |
+
"""PMI prior from a forward on ``[SOS, MASK × block_size]`` with **no
|
| 417 |
+
conditioning at all** — no speaker, no text, no prompt speech, not
|
| 418 |
+
even zero-filled embeddings of those. Captures the model's intrinsic
|
| 419 |
+
marginal over speech tokens at the very start of an utterance.
|
| 420 |
+
|
| 421 |
+
The result depends only on ``(block_size, SOS, MASK, dtype, device)``
|
| 422 |
+
and the (frozen, inference-time) model weights, so we **cache** it
|
| 423 |
+
on ``self._uncond_block_prior_cache`` keyed by that tuple. The actual
|
| 424 |
+
forward runs at most once per ``(block_size, dtype, device)`` triple
|
| 425 |
+
across the entire lifetime of the model object — subsequent
|
| 426 |
+
``generate()`` calls reuse the cached ``(V,)`` tensor. Invalidated
|
| 427 |
+
implicitly when the model is recreated (e.g. on checkpoint reload).
|
| 428 |
+
"""
|
| 429 |
+
cache: dict = getattr(self, "_uncond_block_prior_cache", None)
|
| 430 |
+
if cache is None:
|
| 431 |
+
cache = {}
|
| 432 |
+
self._uncond_block_prior_cache = cache
|
| 433 |
+
key = (int(block_size), int(SOS), int(MASK), str(dtype), str(device))
|
| 434 |
+
if key in cache:
|
| 435 |
+
return cache[key]
|
| 436 |
+
|
| 437 |
+
n_tok = 1 + int(block_size)
|
| 438 |
+
tok_seq = torch.cat(
|
| 439 |
+
[
|
| 440 |
+
torch.full((1, 1), int(SOS), device=device, dtype=torch.long),
|
| 441 |
+
torch.full(
|
| 442 |
+
(1, int(block_size)), int(MASK),
|
| 443 |
+
device=device, dtype=torch.long,
|
| 444 |
+
),
|
| 445 |
+
],
|
| 446 |
+
dim=1,
|
| 447 |
+
)
|
| 448 |
+
seq_emb = self._embed_speech_tokens(tok_seq).to(dtype)
|
| 449 |
+
position_ids = torch.arange(n_tok, device=device).unsqueeze(0)
|
| 450 |
+
|
| 451 |
+
# The FlashInfer engine swaps the LlamaAttention forward in-place;
|
| 452 |
+
# we explicitly route this one-shot prior pass through HuggingFace's
|
| 453 |
+
# SDPA implementation so it never touches the FlashInfer paged cache.
|
| 454 |
+
cfg = self.tfmr.config
|
| 455 |
+
old_impl = getattr(cfg, "_attn_implementation", "sdpa")
|
| 456 |
+
cfg._attn_implementation = "sdpa"
|
| 457 |
+
try:
|
| 458 |
+
out = self.tfmr(
|
| 459 |
+
input_ids=None,
|
| 460 |
+
inputs_embeds=seq_emb,
|
| 461 |
+
position_ids=position_ids,
|
| 462 |
+
use_cache=False,
|
| 463 |
+
return_dict=True,
|
| 464 |
+
)
|
| 465 |
+
finally:
|
| 466 |
+
cfg._attn_implementation = old_impl
|
| 467 |
+
|
| 468 |
+
# Standard token-shift: logits for mask position i come from hidden
|
| 469 |
+
# at position i (SOS → mask 0, mask k-1 → mask k).
|
| 470 |
+
shift_hidden = out.last_hidden_state[:, : int(block_size), :]
|
| 471 |
+
logits = self.speech_head(shift_hidden)
|
| 472 |
+
probs = F.softmax(logits.float(), dim=-1)
|
| 473 |
+
prior = probs.mean(dim=1).squeeze(0).detach()
|
| 474 |
+
cache[key] = prior
|
| 475 |
+
return prior
|
| 476 |
+
|
| 477 |
+
# ------------------------------------------------------------------ #
|
| 478 |
+
# State-dict surgery (lets the model accept either a pure chatterbox
|
| 479 |
+
# checkpoint of size V or a Chatterbox-Flash checkpoint of size V+1)
|
| 480 |
+
# ------------------------------------------------------------------ #
|
| 481 |
+
|
| 482 |
+
def _load_from_state_dict(
|
| 483 |
+
self, state_dict, prefix, *args, **kwargs,
|
| 484 |
+
): # noqa: D401
|
| 485 |
+
key = prefix + "speech_emb.weight"
|
| 486 |
+
if key in state_dict:
|
| 487 |
+
ckpt_w = state_dict[key]
|
| 488 |
+
model_v = self.speech_emb.weight.shape[0]
|
| 489 |
+
if ckpt_w.shape[0] == model_v - 1:
|
| 490 |
+
logger.info(
|
| 491 |
+
"Expanding speech_emb from %d to %d (adding [MASK] token).",
|
| 492 |
+
ckpt_w.shape[0], model_v,
|
| 493 |
+
)
|
| 494 |
+
expanded = self.speech_emb.weight.detach().clone()
|
| 495 |
+
expanded[: ckpt_w.shape[0]] = ckpt_w
|
| 496 |
+
state_dict[key] = expanded
|
| 497 |
+
return super()._load_from_state_dict(state_dict, prefix, *args, **kwargs)
|
| 498 |
+
|
| 499 |
+
# ------------------------------------------------------------------ #
|
| 500 |
+
# Prefix forward (run once per generation, cached in FlashInfer)
|
| 501 |
+
# ------------------------------------------------------------------ #
|
| 502 |
+
|
| 503 |
+
def _build_prefix_emb(
|
| 504 |
+
self,
|
| 505 |
+
cond_emb: Tensor,
|
| 506 |
+
text_tokens: Tensor,
|
| 507 |
+
device: torch.device,
|
| 508 |
+
dtype: torch.dtype,
|
| 509 |
+
) -> Tensor:
|
| 510 |
+
"""Build ``[cond | text | <start_speech>]`` embedding."""
|
| 511 |
+
text_emb = self.text_emb(text_tokens)
|
| 512 |
+
if self.hp.input_pos_emb == "learned":
|
| 513 |
+
text_emb = text_emb + self.text_pos_emb(text_tokens)
|
| 514 |
+
sos = torch.full(
|
| 515 |
+
(1, 1), self.hp.start_speech_token, device=device, dtype=torch.long,
|
| 516 |
+
)
|
| 517 |
+
sos_emb = self._embed_speech_tokens(sos)
|
| 518 |
+
return torch.cat([cond_emb, text_emb, sos_emb], dim=1).to(dtype)
|
| 519 |
+
|
| 520 |
+
# ------------------------------------------------------------------ #
|
| 521 |
+
# Block-diffusion generate
|
| 522 |
+
# ------------------------------------------------------------------ #
|
| 523 |
+
|
| 524 |
+
@torch.inference_mode()
|
| 525 |
+
def generate(
|
| 526 |
+
self,
|
| 527 |
+
*,
|
| 528 |
+
t3_cond: "T3Cond | list[T3Cond] | tuple[T3Cond, ...]",
|
| 529 |
+
text_tokens: "torch.Tensor | list[torch.Tensor]",
|
| 530 |
+
text_token_lens: "torch.Tensor | None" = None,
|
| 531 |
+
total_speech_len: "int | list[int]" = 0,
|
| 532 |
+
num_steps: int = 10,
|
| 533 |
+
temperature: float = 0.6,
|
| 534 |
+
temperature_sampling: Literal["multinomial", "gumbel"] = "multinomial",
|
| 535 |
+
time_shift_tau: float = 0.1,
|
| 536 |
+
omnivoice_schedule_t_shift: float = 0.5,
|
| 537 |
+
cfg_scale: float = 1.0,
|
| 538 |
+
position_temperature: float = 5.0,
|
| 539 |
+
pmi_uncond_prior_precompute: bool = True,
|
| 540 |
+
batch_size: int = 1,
|
| 541 |
+
page_size: int = 16,
|
| 542 |
+
flashinfer_reserve_max_seq: int | None = None,
|
| 543 |
+
use_cuda_graph: bool = True,
|
| 544 |
+
backend: Literal["auto", "flashinfer", "torch"] = "auto",
|
| 545 |
+
) -> Tensor:
|
| 546 |
+
"""Block-wise denoising generation matching the paper's best config.
|
| 547 |
+
|
| 548 |
+
Inputs accept either a single sample (``text_tokens`` is a tensor and
|
| 549 |
+
``t3_cond`` is a :class:`T3Cond`) or a heterogeneous batch
|
| 550 |
+
(``text_tokens`` is a list, ``t3_cond`` is a list of :class:`T3Cond`).
|
| 551 |
+
Both modes are dispatched through the same FlashInfer block-decoding
|
| 552 |
+
loop. Heterogeneous batches pad to ``max(total_speech_len)`` and stop
|
| 553 |
+
each row once it emits ``hp.stop_speech_token``.
|
| 554 |
+
|
| 555 |
+
Decoding-relevant parameters (paper defaults):
|
| 556 |
+
|
| 557 |
+
``num_steps``
|
| 558 |
+
Maximum denoising iterations per block (``K``). Default 10.
|
| 559 |
+
``time_shift_tau``
|
| 560 |
+
Early-decoding aggressiveness in
|
| 561 |
+
``q_k = max(0, 1 - τ·(k+1)/K)``. ``0.0`` disables early decoding
|
| 562 |
+
(every block uses exactly the OmniVoice count schedule). Default
|
| 563 |
+
``0.1`` (paper).
|
| 564 |
+
``omnivoice_schedule_t_shift``
|
| 565 |
+
OmniVoice r_n schedule parameter that controls the per-step
|
| 566 |
+
unmask count floor. Default ``0.5``.
|
| 567 |
+
``cfg_scale``
|
| 568 |
+
CFG strength. When ``> 0`` the forward batch is doubled with a
|
| 569 |
+
null branch (``zero_text_batch`` + ``zero_all`` null prefix +
|
| 570 |
+
text/cond zeroed + speech-x_t duplicated). Token sampling uses
|
| 571 |
+
the guided distribution; PMI scoring is combined on the PMI
|
| 572 |
+
scale via ``(1+w)·pmi_c − w·pmi_u`` (``pmi_cfg``) — this is the
|
| 573 |
+
only supported PMI/CFG combination, no other mode is exposed.
|
| 574 |
+
``position_temperature``
|
| 575 |
+
Gumbel temperature added to PMI for stochastic top-k position
|
| 576 |
+
ranking. ``0`` = deterministic argmax-top-k; default ``5.0``
|
| 577 |
+
(OmniVoice §3.4 T=5).
|
| 578 |
+
``pmi_uncond_prior_precompute``
|
| 579 |
+
When ``True`` (default), the PMI denominator is the
|
| 580 |
+
unconditional block prior computed once via
|
| 581 |
+
:meth:`_compute_uncond_block_prior` and cached on the model.
|
| 582 |
+
When ``False``, falls back to the running per-block marginal
|
| 583 |
+
from step 0.
|
| 584 |
+
|
| 585 |
+
Returns
|
| 586 |
+
-------
|
| 587 |
+
torch.LongTensor
|
| 588 |
+
Speech token ids of shape ``(B, T)`` with
|
| 589 |
+
``T <= max(total_speech_len)``.
|
| 590 |
+
"""
|
| 591 |
+
device = self.device
|
| 592 |
+
BS = self.drf_block_size
|
| 593 |
+
K = num_steps
|
| 594 |
+
MASK = self.mask_token_id
|
| 595 |
+
dtype = next(self.parameters()).dtype
|
| 596 |
+
stop_tok = self.hp.stop_speech_token
|
| 597 |
+
|
| 598 |
+
# ---- cross-text vs same-text batching ----
|
| 599 |
+
_cross_text = isinstance(text_tokens, list)
|
| 600 |
+
if _cross_text:
|
| 601 |
+
B = len(text_tokens)
|
| 602 |
+
_speech_lens: list[int] = (
|
| 603 |
+
list(total_speech_len)
|
| 604 |
+
if isinstance(total_speech_len, list)
|
| 605 |
+
else [total_speech_len] * B
|
| 606 |
+
)
|
| 607 |
+
N = max(_speech_lens) if _speech_lens else 0
|
| 608 |
+
_per_sample_num_blocks = [math.ceil(sl / BS) for sl in _speech_lens]
|
| 609 |
+
num_blocks = max(_per_sample_num_blocks) if _per_sample_num_blocks else 0
|
| 610 |
+
else:
|
| 611 |
+
B = max(1, batch_size)
|
| 612 |
+
_speech_lens = []
|
| 613 |
+
N = total_speech_len if isinstance(total_speech_len, int) else total_speech_len[0]
|
| 614 |
+
num_blocks = math.ceil(N / BS) if N > 0 else 0
|
| 615 |
+
_per_sample_num_blocks = []
|
| 616 |
+
|
| 617 |
+
if N <= 0:
|
| 618 |
+
return torch.empty((B, 0), device=device, dtype=torch.long)
|
| 619 |
+
|
| 620 |
+
B_usr = B
|
| 621 |
+
_ztb = cfg_scale > 0
|
| 622 |
+
B_fwd = (2 * B_usr) if _ztb else B_usr
|
| 623 |
+
|
| 624 |
+
# ---- prefix(es) ----
|
| 625 |
+
t3_is_seq = isinstance(t3_cond, (list, tuple))
|
| 626 |
+
if t3_is_seq and not _cross_text:
|
| 627 |
+
raise ValueError(
|
| 628 |
+
"t3_cond as a list/tuple is only supported when text_tokens is a list.",
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
cond_embs: list[Tensor] | None = None
|
| 632 |
+
cond_emb_single: Tensor | None = None
|
| 633 |
+
if _cross_text:
|
| 634 |
+
if t3_is_seq:
|
| 635 |
+
if len(t3_cond) != len(text_tokens):
|
| 636 |
+
raise ValueError(
|
| 637 |
+
f"len(t3_cond) ({len(t3_cond)}) must match "
|
| 638 |
+
f"len(text_tokens) ({len(text_tokens)}).",
|
| 639 |
+
)
|
| 640 |
+
cond_embs = [self.prepare_conditioning(c) for c in t3_cond]
|
| 641 |
+
else:
|
| 642 |
+
cond_emb_single = self.prepare_conditioning(t3_cond)
|
| 643 |
+
else:
|
| 644 |
+
cond_emb_single = self.prepare_conditioning(t3_cond)
|
| 645 |
+
|
| 646 |
+
prefix_emb_cond: Tensor | list[Tensor]
|
| 647 |
+
prefix_emb_null: Tensor | list[Tensor] | None = None
|
| 648 |
+
_prefix_lens: list[int]
|
| 649 |
+
|
| 650 |
+
if _cross_text:
|
| 651 |
+
cond_list: list[Tensor] = []
|
| 652 |
+
null_list: list[Tensor] = []
|
| 653 |
+
lens: list[int] = []
|
| 654 |
+
for i, tt in enumerate(text_tokens):
|
| 655 |
+
ce = cond_embs[i] if cond_embs is not None else cond_emb_single
|
| 656 |
+
p = self._build_prefix_emb(ce, tt, device, dtype)
|
| 657 |
+
cond_list.append(p)
|
| 658 |
+
lens.append(p.size(1))
|
| 659 |
+
if _ztb:
|
| 660 |
+
ce_null = _cond_emb_zero_all(ce)
|
| 661 |
+
text_emb = self.text_emb(tt)
|
| 662 |
+
if self.hp.input_pos_emb == "learned":
|
| 663 |
+
text_emb = text_emb + self.text_pos_emb(tt)
|
| 664 |
+
text_emb_zero = _zero_text_content_keep_pad(
|
| 665 |
+
text_emb, tt, None,
|
| 666 |
+
)
|
| 667 |
+
sos = torch.full(
|
| 668 |
+
(1, 1), self.hp.start_speech_token,
|
| 669 |
+
device=device, dtype=torch.long,
|
| 670 |
+
)
|
| 671 |
+
sos_emb = self._embed_speech_tokens(sos)
|
| 672 |
+
p_null = torch.cat(
|
| 673 |
+
[ce_null, text_emb_zero, sos_emb], dim=1,
|
| 674 |
+
).to(dtype)
|
| 675 |
+
null_list.append(p_null)
|
| 676 |
+
prefix_emb_cond = cond_list
|
| 677 |
+
lp = max(lens)
|
| 678 |
+
_prefix_lens = list(lens)
|
| 679 |
+
if _ztb:
|
| 680 |
+
prefix_emb_null = null_list
|
| 681 |
+
_prefix_lens = _prefix_lens + [p.size(1) for p in null_list]
|
| 682 |
+
else:
|
| 683 |
+
assert cond_emb_single is not None
|
| 684 |
+
prefix_emb = self._build_prefix_emb(
|
| 685 |
+
cond_emb_single, text_tokens, device, dtype,
|
| 686 |
+
)
|
| 687 |
+
lp = prefix_emb.size(1)
|
| 688 |
+
if B_usr > 1:
|
| 689 |
+
prefix_emb = prefix_emb.expand(B_usr, -1, -1).contiguous()
|
| 690 |
+
prefix_emb_cond = prefix_emb
|
| 691 |
+
_prefix_lens = [lp] * B_fwd
|
| 692 |
+
if _ztb:
|
| 693 |
+
cond_for_null = _cond_emb_zero_all(cond_emb_single)
|
| 694 |
+
text_emb = self.text_emb(text_tokens)
|
| 695 |
+
if self.hp.input_pos_emb == "learned":
|
| 696 |
+
text_emb = text_emb + self.text_pos_emb(text_tokens)
|
| 697 |
+
text_emb_zero = _zero_text_content_keep_pad(
|
| 698 |
+
text_emb, text_tokens, text_token_lens,
|
| 699 |
+
)
|
| 700 |
+
sos = torch.full(
|
| 701 |
+
(1, 1), self.hp.start_speech_token,
|
| 702 |
+
device=device, dtype=torch.long,
|
| 703 |
+
)
|
| 704 |
+
sos_emb = self._embed_speech_tokens(sos)
|
| 705 |
+
p_null = torch.cat(
|
| 706 |
+
[cond_for_null, text_emb_zero, sos_emb], dim=1,
|
| 707 |
+
).to(dtype)
|
| 708 |
+
if B_usr > 1:
|
| 709 |
+
p_null = p_null.expand(B_usr, -1, -1).contiguous()
|
| 710 |
+
prefix_emb_null = p_null
|
| 711 |
+
|
| 712 |
+
# ---- speech buffer (mask everywhere; we fill it block by block) ----
|
| 713 |
+
xt = torch.full((B_fwd, N), MASK, device=device, dtype=torch.long)
|
| 714 |
+
|
| 715 |
+
# ---- precomputed unconditional block prior (PMI denominator) ----
|
| 716 |
+
# Single forward on [SOS, MASK × BS] with no conditioning, lazy-cached
|
| 717 |
+
# on the model. Reused as PMI denominator for every step of every
|
| 718 |
+
# block in this generate() call (and across subsequent calls).
|
| 719 |
+
SOS = int(self.hp.start_speech_token)
|
| 720 |
+
prior_override_v: Tensor | None = None
|
| 721 |
+
if pmi_uncond_prior_precompute:
|
| 722 |
+
prior_override_v = self._compute_uncond_block_prior(
|
| 723 |
+
BS, SOS, MASK, dtype, device,
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
# ---- inference engine (FlashInfer if available, else torch SDPA) ----
|
| 727 |
+
engine_max_seq = max(lp + N + 64, flashinfer_reserve_max_seq or 0)
|
| 728 |
+
cached = self._cached_engine
|
| 729 |
+
if (
|
| 730 |
+
cached is not None
|
| 731 |
+
and cached.can_reuse(
|
| 732 |
+
engine_max_seq, dtype, batch_size=B_fwd, page_size=page_size,
|
| 733 |
+
)
|
| 734 |
+
):
|
| 735 |
+
engine = cached
|
| 736 |
+
engine.reset()
|
| 737 |
+
else:
|
| 738 |
+
if (
|
| 739 |
+
use_cuda_graph
|
| 740 |
+
and cached is not None
|
| 741 |
+
and getattr(cached, "_max_seq_len", 0) < engine_max_seq
|
| 742 |
+
and getattr(cached, "_batch_size", -1) == B_fwd
|
| 743 |
+
and getattr(cached, "_dtype", None) == dtype
|
| 744 |
+
):
|
| 745 |
+
# Grow allocation in 512-token buckets so we don't re-instantiate
|
| 746 |
+
# the engine every time the sequence length nudges up by one.
|
| 747 |
+
engine_max_seq = max(
|
| 748 |
+
((engine_max_seq + 511) // 512) * 512,
|
| 749 |
+
int(getattr(cached, "_max_seq_len", 0) * 1.5),
|
| 750 |
+
)
|
| 751 |
+
engine = build_engine(
|
| 752 |
+
self, engine_max_seq, dtype,
|
| 753 |
+
backend=backend,
|
| 754 |
+
batch_size=B_fwd, page_size=page_size,
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
if _ztb:
|
| 758 |
+
assert prefix_emb_null is not None
|
| 759 |
+
if isinstance(prefix_emb_cond, list):
|
| 760 |
+
assert isinstance(prefix_emb_null, list)
|
| 761 |
+
pfx_for_engine = [*prefix_emb_cond, *prefix_emb_null]
|
| 762 |
+
else:
|
| 763 |
+
assert isinstance(prefix_emb_null, torch.Tensor)
|
| 764 |
+
pfx_for_engine = [
|
| 765 |
+
*[prefix_emb_cond[bi : bi + 1] for bi in range(B_usr)],
|
| 766 |
+
*[prefix_emb_null[bi : bi + 1] for bi in range(B_usr)],
|
| 767 |
+
]
|
| 768 |
+
shift_ctx = engine.prefix_forward(pfx_for_engine)
|
| 769 |
+
else:
|
| 770 |
+
shift_ctx = engine.prefix_forward(prefix_emb_cond)
|
| 771 |
+
|
| 772 |
+
if use_cuda_graph:
|
| 773 |
+
# ``capture_cuda_graph`` is a no-op on the torch SDPA fallback.
|
| 774 |
+
engine.capture_cuda_graph(BS, speech_head=self.speech_head)
|
| 775 |
+
self._cached_engine = engine
|
| 776 |
+
|
| 777 |
+
# ---- block-by-block decoding ----
|
| 778 |
+
# ``_row_eos[bi]`` — row has emitted EOS, skip remaining blocks.
|
| 779 |
+
# ``_block_done[bi]`` — inner k-loop finished for this row in the
|
| 780 |
+
# current block (reset at every block boundary; not a sequence-level
|
| 781 |
+
# flag).
|
| 782 |
+
_row_eos = (
|
| 783 |
+
torch.zeros(B_usr, dtype=torch.bool, device=device)
|
| 784 |
+
if B_usr > 1
|
| 785 |
+
else None
|
| 786 |
+
)
|
| 787 |
+
_block_done = (
|
| 788 |
+
torch.zeros(B_usr, dtype=torch.bool, device=device)
|
| 789 |
+
if B_usr > 1
|
| 790 |
+
else None
|
| 791 |
+
)
|
| 792 |
+
block_marginal_priors: list[Tensor | None] = [None] * B_usr
|
| 793 |
+
|
| 794 |
+
for b_idx in range(num_blocks):
|
| 795 |
+
bs_ = b_idx * BS
|
| 796 |
+
be_ = min(bs_ + BS, N)
|
| 797 |
+
bl = be_ - bs_
|
| 798 |
+
|
| 799 |
+
# OmniVoice r_n count schedule for this block's K steps. Used as
|
| 800 |
+
# the per-step unmask **floor** by the pmi_count_early outlier
|
| 801 |
+
# rule. Computed once per block since ``bl`` only changes at the
|
| 802 |
+
# tail.
|
| 803 |
+
_omnivoice_sched = _omnivoice_unmask_schedule(
|
| 804 |
+
n_total_mask=bl,
|
| 805 |
+
num_steps=K,
|
| 806 |
+
t_shift=float(omnivoice_schedule_t_shift),
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
if _block_done is not None:
|
| 810 |
+
_block_done.zero_()
|
| 811 |
+
if _cross_text:
|
| 812 |
+
for bi in range(B_usr):
|
| 813 |
+
if (
|
| 814 |
+
b_idx >= _per_sample_num_blocks[bi]
|
| 815 |
+
or (_row_eos is not None and bool(_row_eos[bi]))
|
| 816 |
+
):
|
| 817 |
+
_block_done[bi] = True
|
| 818 |
+
elif _row_eos is not None:
|
| 819 |
+
for bi in range(B_usr):
|
| 820 |
+
if bool(_row_eos[bi]):
|
| 821 |
+
_block_done[bi] = True
|
| 822 |
+
|
| 823 |
+
for k in range(K):
|
| 824 |
+
# Per-row cache-start positions (cross-text & null-prefix paths
|
| 825 |
+
# may differ between rows; same-text reuses a scalar).
|
| 826 |
+
if _cross_text:
|
| 827 |
+
cache_start: int | list[int] = [
|
| 828 |
+
_prefix_lens[bi] + bs_ for bi in range(B_fwd)
|
| 829 |
+
]
|
| 830 |
+
else:
|
| 831 |
+
cache_start = lp + bs_
|
| 832 |
+
|
| 833 |
+
sp_blk = self._embed_speech_tokens(xt[:, bs_:be_]).to(dtype)
|
| 834 |
+
|
| 835 |
+
use_graph = engine.has_cuda_graph and bl == BS
|
| 836 |
+
if use_graph:
|
| 837 |
+
if k == 0:
|
| 838 |
+
engine.set_shift_ctx(shift_ctx)
|
| 839 |
+
blk_hidden, graph_logits = engine.block_forward_graph(
|
| 840 |
+
sp_blk, cache_start,
|
| 841 |
+
)
|
| 842 |
+
if graph_logits is not None:
|
| 843 |
+
blk_logits = graph_logits
|
| 844 |
+
else:
|
| 845 |
+
shift_hidden = torch.cat(
|
| 846 |
+
[shift_ctx, blk_hidden[:, : bl - 1]], dim=1,
|
| 847 |
+
)
|
| 848 |
+
blk_logits = self.speech_head(shift_hidden)
|
| 849 |
+
else:
|
| 850 |
+
blk_hidden = engine.block_forward(sp_blk, cache_start)
|
| 851 |
+
shift_hidden = torch.cat(
|
| 852 |
+
[shift_ctx, blk_hidden[:, : bl - 1]], dim=1,
|
| 853 |
+
)
|
| 854 |
+
blk_logits = self.speech_head(shift_hidden)
|
| 855 |
+
|
| 856 |
+
pmi_cfg_probs_c: Tensor | None = None
|
| 857 |
+
pmi_cfg_probs_u: Tensor | None = None
|
| 858 |
+
if _ztb:
|
| 859 |
+
logits_cond = blk_logits[:B_usr]
|
| 860 |
+
logits_uncond = blk_logits[B_usr : 2 * B_usr]
|
| 861 |
+
# Always pmi_cfg: PMI scores are combined on the PMI
|
| 862 |
+
# scale via (1+w)·pmi_c − w·pmi_u. No other CFG/PMI
|
| 863 |
+
# combination is exposed.
|
| 864 |
+
pmi_cfg_probs_c = F.softmax(logits_cond, dim=-1)
|
| 865 |
+
pmi_cfg_probs_u = F.softmax(logits_uncond, dim=-1)
|
| 866 |
+
blk_logits = apply_zero_text_cfg_from_logits(
|
| 867 |
+
logits_cond, logits_uncond, cfg_scale,
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
step_break = False
|
| 871 |
+
for bi in range(B_usr):
|
| 872 |
+
if _block_done is not None and _block_done[bi]:
|
| 873 |
+
continue
|
| 874 |
+
pmi_c_bi = None if pmi_cfg_probs_c is None else pmi_cfg_probs_c[bi]
|
| 875 |
+
pmi_u_bi = None if pmi_cfg_probs_u is None else pmi_cfg_probs_u[bi]
|
| 876 |
+
r = _pmi_count_early_step_unmask(
|
| 877 |
+
blk_logits[bi],
|
| 878 |
+
xt[bi, bs_:be_],
|
| 879 |
+
bl=bl,
|
| 880 |
+
k=k, K=K,
|
| 881 |
+
MASK=MASK, device=device,
|
| 882 |
+
temperature=temperature,
|
| 883 |
+
temperature_sampling=temperature_sampling,
|
| 884 |
+
omnivoice_unmask_schedule_k=_omnivoice_sched[k],
|
| 885 |
+
time_shift_tau=time_shift_tau,
|
| 886 |
+
prior_override_v=prior_override_v,
|
| 887 |
+
block_marginal_prior=block_marginal_priors[bi],
|
| 888 |
+
pmi_cfg_probs_c_bl=pmi_c_bi,
|
| 889 |
+
pmi_cfg_probs_u_bl=pmi_u_bi,
|
| 890 |
+
cfg_scale_for_pmi=cfg_scale,
|
| 891 |
+
position_temperature=position_temperature,
|
| 892 |
+
)
|
| 893 |
+
xt[bi, bs_:be_] = r["xt_step"]
|
| 894 |
+
block_marginal_priors[bi] = r["block_marginal_prior"]
|
| 895 |
+
if _ztb:
|
| 896 |
+
xt[B_usr + bi, bs_:be_] = r["xt_step"]
|
| 897 |
+
row_hit_eos = bool(
|
| 898 |
+
(xt[bi, bs_:be_] == stop_tok).any().item(),
|
| 899 |
+
)
|
| 900 |
+
if r["n_mask"] == 0 or row_hit_eos:
|
| 901 |
+
if _block_done is not None:
|
| 902 |
+
_block_done[bi] = True
|
| 903 |
+
if row_hit_eos and _row_eos is not None:
|
| 904 |
+
_row_eos[bi] = True
|
| 905 |
+
if B_usr == 1 and row_hit_eos:
|
| 906 |
+
step_break = True
|
| 907 |
+
elif B_usr == 1 and r["n_mask"] == 0:
|
| 908 |
+
step_break = True
|
| 909 |
+
if step_break:
|
| 910 |
+
break
|
| 911 |
+
if _block_done is not None and bool(_block_done.all().item()):
|
| 912 |
+
break
|
| 913 |
+
|
| 914 |
+
# Mid-block finalize: write the committed (possibly partial) tokens
|
| 915 |
+
# of this block to the KV cache so the next block sees clean context.
|
| 916 |
+
if b_idx < num_blocks - 1:
|
| 917 |
+
fin_emb = self._embed_speech_tokens(xt[:, bs_:be_]).to(dtype)
|
| 918 |
+
if _cross_text:
|
| 919 |
+
fin_cs: int | list[int] = [
|
| 920 |
+
_prefix_lens[bi] + bs_ for bi in range(B_fwd)
|
| 921 |
+
]
|
| 922 |
+
else:
|
| 923 |
+
fin_cs = lp + bs_
|
| 924 |
+
fin_hidden = engine.block_forward(fin_emb, fin_cs)
|
| 925 |
+
engine.advance_cache(bl)
|
| 926 |
+
shift_ctx = fin_hidden[:, bl - 1 : bl, :].clone()
|
| 927 |
+
|
| 928 |
+
# Early termination if all (same-text) rows have finished.
|
| 929 |
+
if B_usr == 1:
|
| 930 |
+
eos_pos = (xt[0, bs_:be_] == stop_tok).nonzero(as_tuple=True)[0]
|
| 931 |
+
if len(eos_pos) > 0:
|
| 932 |
+
return xt[:B_usr, : bs_ + eos_pos[0].item()]
|
| 933 |
+
elif _row_eos is not None and bool(_row_eos.all().item()):
|
| 934 |
+
break
|
| 935 |
+
|
| 936 |
+
return xt[:B_usr]
|
runtime/chatterbox-flash/chatterbox_flash/nvfp4.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Canonical, source-free NVFP4 loader for Chatterbox-Flash.
|
| 3 |
+
|
| 4 |
+
This module is the runtime counterpart to
|
| 5 |
+
``export_chatterbox_flash_w4a16_hf.py``. It constructs T3 on the meta device,
|
| 6 |
+
loads only the BF16 non-projection tensors, prepares canonical NVFP4 tensors
|
| 7 |
+
one projection at a time, and attaches an engine-local projection provider.
|
| 8 |
+
No BF16 transformer projection is materialized and global ``F.linear`` is
|
| 9 |
+
never patched.
|
| 10 |
+
|
| 11 |
+
The canonical artifact remains independent of FlashInfer's prepared layout:
|
| 12 |
+
``prepare_bf16_fp4_weights`` runs at load time, while ``nvfp4_quantize`` is
|
| 13 |
+
intentionally neither required nor called. The provider is attached to the
|
| 14 |
+
model rather than an individual engine, so cache-size engine rebuilds reuse
|
| 15 |
+
the same prepared weights.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import hashlib
|
| 21 |
+
import json
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from typing import Any
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from safetensors import safe_open
|
| 28 |
+
from safetensors.torch import load_file
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
ARTIFACT_FILENAME = "t3_nvfp4.safetensors"
|
| 32 |
+
MANIFEST_FILENAME = "nvfp4_config.json"
|
| 33 |
+
BASE_REPO_ID = "ResembleAI/chatterbox-flash"
|
| 34 |
+
BASE_CHECKPOINT_REVISION = "4385507288b8197e6dab8b4e6b1603328d549d9d"
|
| 35 |
+
BASE_ASSET_FILENAMES = (
|
| 36 |
+
"ve.safetensors",
|
| 37 |
+
"s3gen.safetensors",
|
| 38 |
+
"tokenizer.json",
|
| 39 |
+
)
|
| 40 |
+
SUPPORTED_RELEASE_BLOCK_SIZES = (16, 24)
|
| 41 |
+
PROJECTION_KINDS = ("gate_up", "down", "qkv", "o")
|
| 42 |
+
PROJECTION_COMPONENTS = ("weight_fp4", "scale_128x4_e4m3", "alpha")
|
| 43 |
+
EXPECTED_LAYERS = 30
|
| 44 |
+
EXPECTED_PREPARED_BYTES = 283_116_000
|
| 45 |
+
EXPECTED_NONPROJECTION_TENSORS = 82
|
| 46 |
+
EXPECTED_NONPROJECTION_BYTES = 58_179_584
|
| 47 |
+
PROJECTION_DIMENSIONS = {
|
| 48 |
+
"gate_up": (8_192, 1_024),
|
| 49 |
+
"down": (1_024, 4_096),
|
| 50 |
+
"qkv": (3_072, 1_024),
|
| 51 |
+
"o": (1_024, 1_024),
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _tensor_bytes(tensor: torch.Tensor) -> int:
|
| 56 |
+
return tensor.numel() * tensor.element_size()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _canonical_cuda_device(device: torch.device | str) -> torch.device:
|
| 60 |
+
"""Resolve bare ``cuda`` to the indexed device used by model tensors."""
|
| 61 |
+
resolved = torch.device(device)
|
| 62 |
+
if resolved.type == "cuda" and resolved.index is None:
|
| 63 |
+
resolved = torch.device("cuda", torch.cuda.current_device())
|
| 64 |
+
return resolved
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _sha256_file(path: Path, chunk_bytes: int = 8 * 1024 * 1024) -> str:
|
| 68 |
+
digest = hashlib.sha256()
|
| 69 |
+
with path.open("rb") as handle:
|
| 70 |
+
while chunk := handle.read(chunk_bytes):
|
| 71 |
+
digest.update(chunk)
|
| 72 |
+
return digest.hexdigest()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _is_projection_source_key(key: str) -> bool:
|
| 76 |
+
parts = key.split(".")
|
| 77 |
+
if (
|
| 78 |
+
len(parts) != 6
|
| 79 |
+
or parts[0] != "tfmr"
|
| 80 |
+
or parts[1] != "layers"
|
| 81 |
+
or not parts[2].isdigit()
|
| 82 |
+
or parts[-1] != "weight"
|
| 83 |
+
):
|
| 84 |
+
return False
|
| 85 |
+
module, projection = parts[3], parts[4]
|
| 86 |
+
return (
|
| 87 |
+
module == "self_attn"
|
| 88 |
+
and projection in {"q_proj", "k_proj", "v_proj", "o_proj"}
|
| 89 |
+
) or (
|
| 90 |
+
module == "mlp"
|
| 91 |
+
and projection in {"gate_proj", "up_proj", "down_proj"}
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _canonical_shapes(kind: str) -> dict[str, tuple[int, ...]]:
|
| 96 |
+
output_features, input_features = PROJECTION_DIMENSIONS[kind]
|
| 97 |
+
return {
|
| 98 |
+
"weight_fp4": (output_features, input_features // 2),
|
| 99 |
+
"scale_128x4_e4m3": (output_features, input_features // 16),
|
| 100 |
+
"alpha": (1,),
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _resolve_package(
|
| 105 |
+
package: str | Path,
|
| 106 |
+
manifest_path: str | Path | None,
|
| 107 |
+
) -> tuple[Path, Path, dict[str, Any]]:
|
| 108 |
+
package_path = Path(package).expanduser().resolve()
|
| 109 |
+
if package_path.is_dir():
|
| 110 |
+
artifact_path = package_path / ARTIFACT_FILENAME
|
| 111 |
+
default_manifest = package_path / MANIFEST_FILENAME
|
| 112 |
+
else:
|
| 113 |
+
artifact_path = package_path
|
| 114 |
+
default_manifest = package_path.with_name(MANIFEST_FILENAME)
|
| 115 |
+
config_path = (
|
| 116 |
+
Path(manifest_path).expanduser().resolve()
|
| 117 |
+
if manifest_path is not None
|
| 118 |
+
else default_manifest
|
| 119 |
+
)
|
| 120 |
+
if not artifact_path.is_file():
|
| 121 |
+
raise FileNotFoundError(f"Missing canonical NVFP4 artifact: {artifact_path}")
|
| 122 |
+
if not config_path.is_file():
|
| 123 |
+
raise FileNotFoundError(f"Missing canonical NVFP4 manifest: {config_path}")
|
| 124 |
+
manifest = json.loads(config_path.read_text(encoding="utf-8"))
|
| 125 |
+
if manifest.get("format") != "chatterbox-flash-canonical-nvfp4":
|
| 126 |
+
raise RuntimeError(f"Unsupported NVFP4 format in {config_path}")
|
| 127 |
+
if manifest.get("format_version") != 1:
|
| 128 |
+
raise RuntimeError(
|
| 129 |
+
f"Unsupported NVFP4 format version: {manifest.get('format_version')!r}"
|
| 130 |
+
)
|
| 131 |
+
declared_filename = manifest.get("artifact", {}).get("filename")
|
| 132 |
+
if declared_filename != artifact_path.name:
|
| 133 |
+
raise RuntimeError(
|
| 134 |
+
f"Manifest declares artifact {declared_filename!r}, "
|
| 135 |
+
f"but loader received {artifact_path.name!r}"
|
| 136 |
+
)
|
| 137 |
+
return artifact_path, config_path, manifest
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _verify_base_assets(
|
| 141 |
+
paths: dict[str, Path],
|
| 142 |
+
manifest: dict[str, Any],
|
| 143 |
+
) -> None:
|
| 144 |
+
"""Verify every unchanged base asset before it reaches a model loader."""
|
| 145 |
+
expected_files = manifest.get("base_model", {}).get("files", {})
|
| 146 |
+
for filename in BASE_ASSET_FILENAMES:
|
| 147 |
+
path = paths.get(filename)
|
| 148 |
+
record = expected_files.get(filename)
|
| 149 |
+
if path is None or not path.is_file():
|
| 150 |
+
raise FileNotFoundError(f"Missing base-model asset: {filename}")
|
| 151 |
+
if not isinstance(record, dict):
|
| 152 |
+
raise RuntimeError(f"Manifest has no fingerprint for {filename}")
|
| 153 |
+
expected_bytes = record.get("bytes")
|
| 154 |
+
actual_bytes = path.stat().st_size
|
| 155 |
+
if actual_bytes != expected_bytes:
|
| 156 |
+
raise RuntimeError(
|
| 157 |
+
f"Base asset size mismatch for {filename}: expected "
|
| 158 |
+
f"{expected_bytes}, found {actual_bytes}"
|
| 159 |
+
)
|
| 160 |
+
expected_sha256 = record.get("sha256")
|
| 161 |
+
actual_sha256 = _sha256_file(path)
|
| 162 |
+
if not expected_sha256 or actual_sha256 != expected_sha256:
|
| 163 |
+
raise RuntimeError(
|
| 164 |
+
f"Base asset SHA-256 mismatch for {filename}: expected "
|
| 165 |
+
f"{expected_sha256}, found {actual_sha256}"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
@dataclass(frozen=True)
|
| 170 |
+
class PreparedProjection:
|
| 171 |
+
kind: str
|
| 172 |
+
layer: int
|
| 173 |
+
input_features: int
|
| 174 |
+
output_features: int
|
| 175 |
+
prepared: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
|
| 176 |
+
packed_bytes: int
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class CanonicalNVFP4ProjectionProvider:
|
| 180 |
+
"""FlashInfer projection provider prepared directly from canonical FP4."""
|
| 181 |
+
|
| 182 |
+
source_free = True
|
| 183 |
+
|
| 184 |
+
def __init__(
|
| 185 |
+
self,
|
| 186 |
+
artifact_path: str | Path,
|
| 187 |
+
*,
|
| 188 |
+
device: torch.device | str,
|
| 189 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 190 |
+
autotune_cache: str | Path | None = None,
|
| 191 |
+
) -> None:
|
| 192 |
+
import flashinfer
|
| 193 |
+
|
| 194 |
+
self.device = _canonical_cuda_device(device)
|
| 195 |
+
self.dtype = dtype
|
| 196 |
+
self.backend = "cute-dsl"
|
| 197 |
+
self.artifact_path = Path(artifact_path).expanduser().resolve()
|
| 198 |
+
self.autotune_cache = (
|
| 199 |
+
Path(autotune_cache).expanduser().resolve()
|
| 200 |
+
if autotune_cache is not None
|
| 201 |
+
else None
|
| 202 |
+
)
|
| 203 |
+
if self.device.type != "cuda":
|
| 204 |
+
raise RuntimeError("Canonical NVFP4 projections require a CUDA device")
|
| 205 |
+
if dtype is not torch.bfloat16:
|
| 206 |
+
raise RuntimeError(
|
| 207 |
+
f"Canonical W4A16 requires BF16 activations, found {dtype}"
|
| 208 |
+
)
|
| 209 |
+
if not torch.cuda.is_available():
|
| 210 |
+
raise RuntimeError("CUDA is unavailable")
|
| 211 |
+
capability = torch.cuda.get_device_capability(self.device)
|
| 212 |
+
if capability not in {(12, 0), (12, 1)}:
|
| 213 |
+
raise RuntimeError(
|
| 214 |
+
"The canonical CuTe-DSL NVFP4 profile is validated only on "
|
| 215 |
+
f"SM120/SM121, found compute capability {capability}"
|
| 216 |
+
)
|
| 217 |
+
required = (
|
| 218 |
+
"autotune",
|
| 219 |
+
"prepare_bf16_fp4_weights",
|
| 220 |
+
"mm_bf16_fp4",
|
| 221 |
+
)
|
| 222 |
+
missing = [name for name in required if not hasattr(flashinfer, name)]
|
| 223 |
+
if missing:
|
| 224 |
+
raise RuntimeError(
|
| 225 |
+
f"FlashInfer {getattr(flashinfer, '__version__', 'unknown')} "
|
| 226 |
+
f"lacks required runtime APIs: {missing}"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
self._flashinfer = flashinfer
|
| 230 |
+
self._prepared: dict[str, list[PreparedProjection]] = {
|
| 231 |
+
kind: [] for kind in PROJECTION_KINDS
|
| 232 |
+
}
|
| 233 |
+
self._tuned_shapes: set[tuple[int, int]] = set()
|
| 234 |
+
self._load_canonical_projections()
|
| 235 |
+
|
| 236 |
+
def _load_canonical_projections(self) -> None:
|
| 237 |
+
expected_dtypes = {
|
| 238 |
+
"weight_fp4": torch.uint8,
|
| 239 |
+
"scale_128x4_e4m3": torch.uint8,
|
| 240 |
+
"alpha": torch.float32,
|
| 241 |
+
}
|
| 242 |
+
with safe_open(
|
| 243 |
+
str(self.artifact_path), framework="pt", device="cpu",
|
| 244 |
+
) as handle:
|
| 245 |
+
available = set(handle.keys())
|
| 246 |
+
for layer in range(EXPECTED_LAYERS):
|
| 247 |
+
for kind in PROJECTION_KINDS:
|
| 248 |
+
prefix = f"nvfp4.layers.{layer:02d}.{kind}"
|
| 249 |
+
keys = {
|
| 250 |
+
component: f"{prefix}.{component}"
|
| 251 |
+
for component in PROJECTION_COMPONENTS
|
| 252 |
+
}
|
| 253 |
+
missing = [key for key in keys.values() if key not in available]
|
| 254 |
+
if missing:
|
| 255 |
+
raise RuntimeError(
|
| 256 |
+
f"Canonical artifact is missing projection tensors: {missing}"
|
| 257 |
+
)
|
| 258 |
+
expected_shapes = _canonical_shapes(kind)
|
| 259 |
+
canonical: dict[str, torch.Tensor] = {}
|
| 260 |
+
for component in PROJECTION_COMPONENTS:
|
| 261 |
+
tensor = handle.get_tensor(keys[component])
|
| 262 |
+
if tuple(tensor.shape) != expected_shapes[component]:
|
| 263 |
+
raise RuntimeError(
|
| 264 |
+
f"{keys[component]} shape {tuple(tensor.shape)} != "
|
| 265 |
+
f"{expected_shapes[component]}"
|
| 266 |
+
)
|
| 267 |
+
if tensor.dtype is not expected_dtypes[component]:
|
| 268 |
+
raise RuntimeError(
|
| 269 |
+
f"{keys[component]} dtype {tensor.dtype} != "
|
| 270 |
+
f"{expected_dtypes[component]}"
|
| 271 |
+
)
|
| 272 |
+
canonical[component] = tensor.to(
|
| 273 |
+
device=self.device, non_blocking=False,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# This is preparation, not quantization: canonical FP4
|
| 277 |
+
# codes/scales are consumed directly, one projection at a
|
| 278 |
+
# time. Synchronizing before advancing bounds transient
|
| 279 |
+
# device ownership to this projection plus prepared output.
|
| 280 |
+
prepared_raw = self._flashinfer.prepare_bf16_fp4_weights(
|
| 281 |
+
canonical["weight_fp4"],
|
| 282 |
+
canonical["scale_128x4_e4m3"],
|
| 283 |
+
canonical["alpha"],
|
| 284 |
+
backend=self.backend,
|
| 285 |
+
)
|
| 286 |
+
if len(prepared_raw) != 3 or any(
|
| 287 |
+
tensor is None for tensor in prepared_raw
|
| 288 |
+
):
|
| 289 |
+
raise RuntimeError(
|
| 290 |
+
f"prepare_bf16_fp4_weights returned an invalid tuple "
|
| 291 |
+
f"for {kind}[{layer}]"
|
| 292 |
+
)
|
| 293 |
+
prepared = tuple(prepared_raw)
|
| 294 |
+
torch.cuda.synchronize(self.device)
|
| 295 |
+
output_features, input_features = PROJECTION_DIMENSIONS[kind]
|
| 296 |
+
record = PreparedProjection(
|
| 297 |
+
kind=kind,
|
| 298 |
+
layer=layer,
|
| 299 |
+
input_features=input_features,
|
| 300 |
+
output_features=output_features,
|
| 301 |
+
prepared=prepared,
|
| 302 |
+
packed_bytes=sum(_tensor_bytes(tensor) for tensor in prepared),
|
| 303 |
+
)
|
| 304 |
+
self._prepared[kind].append(record)
|
| 305 |
+
del canonical, prepared_raw, prepared
|
| 306 |
+
|
| 307 |
+
packed_bytes = sum(
|
| 308 |
+
record.packed_bytes
|
| 309 |
+
for records in self._prepared.values()
|
| 310 |
+
for record in records
|
| 311 |
+
)
|
| 312 |
+
if packed_bytes != EXPECTED_PREPARED_BYTES:
|
| 313 |
+
raise RuntimeError(
|
| 314 |
+
f"Prepared projection payload {packed_bytes} bytes != "
|
| 315 |
+
f"{EXPECTED_PREPARED_BYTES}"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
def validate_engine(
|
| 319 |
+
self,
|
| 320 |
+
*,
|
| 321 |
+
device: torch.device,
|
| 322 |
+
dtype: torch.dtype,
|
| 323 |
+
num_layers: int,
|
| 324 |
+
) -> None:
|
| 325 |
+
if _canonical_cuda_device(device) != self.device:
|
| 326 |
+
raise RuntimeError(
|
| 327 |
+
f"Projection provider is on {self.device}, engine requested {device}"
|
| 328 |
+
)
|
| 329 |
+
if dtype is not self.dtype:
|
| 330 |
+
raise RuntimeError(
|
| 331 |
+
f"Projection provider uses {self.dtype}, engine requested {dtype}"
|
| 332 |
+
)
|
| 333 |
+
if num_layers != EXPECTED_LAYERS or any(
|
| 334 |
+
len(records) != num_layers for records in self._prepared.values()
|
| 335 |
+
):
|
| 336 |
+
raise RuntimeError(
|
| 337 |
+
f"Projection provider does not cover all {num_layers} layers"
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
def prepare_for_engine(self, *, batch_size: int, block_size: int) -> None:
|
| 341 |
+
shape = (int(batch_size), int(block_size))
|
| 342 |
+
if shape in self._tuned_shapes:
|
| 343 |
+
return
|
| 344 |
+
if self.autotune_cache is not None:
|
| 345 |
+
self.autotune_cache.parent.mkdir(parents=True, exist_ok=True)
|
| 346 |
+
m = shape[0] * shape[1]
|
| 347 |
+
representatives = [self._prepared[kind][0] for kind in PROJECTION_KINDS]
|
| 348 |
+
with self._flashinfer.autotune(
|
| 349 |
+
True,
|
| 350 |
+
cache=(str(self.autotune_cache) if self.autotune_cache else None),
|
| 351 |
+
tuning_buckets=(m,),
|
| 352 |
+
round_up=False,
|
| 353 |
+
):
|
| 354 |
+
for record in representatives:
|
| 355 |
+
activation = torch.zeros(
|
| 356 |
+
(m, record.input_features),
|
| 357 |
+
device=self.device,
|
| 358 |
+
dtype=self.dtype,
|
| 359 |
+
)
|
| 360 |
+
self._run(record, activation)
|
| 361 |
+
torch.cuda.synchronize(self.device)
|
| 362 |
+
self._tuned_shapes.add(shape)
|
| 363 |
+
|
| 364 |
+
def _run(
|
| 365 |
+
self,
|
| 366 |
+
record: PreparedProjection,
|
| 367 |
+
activation: torch.Tensor,
|
| 368 |
+
) -> torch.Tensor:
|
| 369 |
+
original_shape = activation.shape
|
| 370 |
+
flattened = activation.reshape(-1, record.input_features)
|
| 371 |
+
output = self._flashinfer.mm_bf16_fp4(
|
| 372 |
+
flattened,
|
| 373 |
+
*record.prepared,
|
| 374 |
+
backend=self.backend,
|
| 375 |
+
out_dtype=self.dtype,
|
| 376 |
+
)
|
| 377 |
+
return output.view(*original_shape[:-1], record.output_features)
|
| 378 |
+
|
| 379 |
+
def project(
|
| 380 |
+
self,
|
| 381 |
+
kind: str,
|
| 382 |
+
layer_idx: int,
|
| 383 |
+
activation: torch.Tensor,
|
| 384 |
+
) -> torch.Tensor:
|
| 385 |
+
try:
|
| 386 |
+
record = self._prepared[kind][layer_idx]
|
| 387 |
+
except (KeyError, IndexError) as exc:
|
| 388 |
+
raise RuntimeError(
|
| 389 |
+
f"Unknown canonical projection {kind}[{layer_idx}]"
|
| 390 |
+
) from exc
|
| 391 |
+
if activation.device != self.device or activation.dtype is not self.dtype:
|
| 392 |
+
raise RuntimeError(
|
| 393 |
+
f"{kind}[{layer_idx}] expected {self.device}/{self.dtype}, "
|
| 394 |
+
f"found {activation.device}/{activation.dtype}"
|
| 395 |
+
)
|
| 396 |
+
if activation.shape[-1] != record.input_features:
|
| 397 |
+
raise RuntimeError(
|
| 398 |
+
f"{kind}[{layer_idx}] expected K={record.input_features}, "
|
| 399 |
+
f"found {activation.shape[-1]}"
|
| 400 |
+
)
|
| 401 |
+
return self._run(record, activation)
|
| 402 |
+
|
| 403 |
+
def report(self) -> dict[str, Any]:
|
| 404 |
+
return {
|
| 405 |
+
"format": "chatterbox-flash-canonical-nvfp4",
|
| 406 |
+
"source_free": True,
|
| 407 |
+
"artifact": str(self.artifact_path),
|
| 408 |
+
"backend": self.backend,
|
| 409 |
+
"device": str(self.device),
|
| 410 |
+
"dtype": str(self.dtype),
|
| 411 |
+
"layers": EXPECTED_LAYERS,
|
| 412 |
+
"packed_linear_count": sum(map(len, self._prepared.values())),
|
| 413 |
+
"prepared_bytes": sum(
|
| 414 |
+
record.packed_bytes
|
| 415 |
+
for records in self._prepared.values()
|
| 416 |
+
for record in records
|
| 417 |
+
),
|
| 418 |
+
"tuned_engine_shapes": sorted(self._tuned_shapes),
|
| 419 |
+
"calls_nvfp4_quantize": False,
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def _load_source_free_model_state(
|
| 424 |
+
artifact_path: Path,
|
| 425 |
+
*,
|
| 426 |
+
device: torch.device,
|
| 427 |
+
dtype: torch.dtype,
|
| 428 |
+
drf_block_size: int,
|
| 429 |
+
):
|
| 430 |
+
from chatterbox_flash.model import ChatterboxFlashT3
|
| 431 |
+
|
| 432 |
+
with torch.device("meta"):
|
| 433 |
+
model = ChatterboxFlashT3(drf_block_size=drf_block_size)
|
| 434 |
+
|
| 435 |
+
with safe_open(
|
| 436 |
+
str(artifact_path), framework="pt", device="cpu",
|
| 437 |
+
) as handle:
|
| 438 |
+
keys = set(handle.keys())
|
| 439 |
+
forbidden = sorted(key for key in keys if _is_projection_source_key(key))
|
| 440 |
+
if forbidden:
|
| 441 |
+
raise RuntimeError(
|
| 442 |
+
"Canonical artifact unexpectedly contains BF16 projection sources: "
|
| 443 |
+
f"{forbidden[:3]}"
|
| 444 |
+
)
|
| 445 |
+
nonprojection_keys = sorted(
|
| 446 |
+
key for key in keys if not key.startswith("nvfp4.")
|
| 447 |
+
)
|
| 448 |
+
if len(nonprojection_keys) != EXPECTED_NONPROJECTION_TENSORS:
|
| 449 |
+
raise RuntimeError(
|
| 450 |
+
f"Expected {EXPECTED_NONPROJECTION_TENSORS} non-projection tensors, "
|
| 451 |
+
f"found {len(nonprojection_keys)}"
|
| 452 |
+
)
|
| 453 |
+
state = {
|
| 454 |
+
key: handle.get_tensor(key).to(
|
| 455 |
+
device=device,
|
| 456 |
+
dtype=dtype,
|
| 457 |
+
non_blocking=False,
|
| 458 |
+
)
|
| 459 |
+
for key in nonprojection_keys
|
| 460 |
+
}
|
| 461 |
+
state_bytes = sum(_tensor_bytes(tensor) for tensor in state.values())
|
| 462 |
+
if state_bytes != EXPECTED_NONPROJECTION_BYTES:
|
| 463 |
+
raise RuntimeError(
|
| 464 |
+
f"Non-projection payload {state_bytes} bytes != "
|
| 465 |
+
f"{EXPECTED_NONPROJECTION_BYTES} bytes"
|
| 466 |
+
)
|
| 467 |
+
incompatible = model.load_state_dict(state, strict=False, assign=True)
|
| 468 |
+
if incompatible.unexpected_keys:
|
| 469 |
+
raise RuntimeError(
|
| 470 |
+
f"Unexpected non-projection keys: {incompatible.unexpected_keys}"
|
| 471 |
+
)
|
| 472 |
+
unexpected_missing = [
|
| 473 |
+
key for key in incompatible.missing_keys
|
| 474 |
+
if not _is_projection_source_key(key)
|
| 475 |
+
]
|
| 476 |
+
if unexpected_missing:
|
| 477 |
+
raise RuntimeError(
|
| 478 |
+
f"Artifact is missing non-projection model keys: {unexpected_missing}"
|
| 479 |
+
)
|
| 480 |
+
del state
|
| 481 |
+
|
| 482 |
+
rotary = model.tfmr.rotary_emb
|
| 483 |
+
rotary.inv_freq = handle.get_tensor(
|
| 484 |
+
"nvfp4.runtime.rotary.inv_freq"
|
| 485 |
+
).to(device=device, dtype=dtype)
|
| 486 |
+
rotary.original_inv_freq = handle.get_tensor(
|
| 487 |
+
"nvfp4.runtime.rotary.original_inv_freq"
|
| 488 |
+
).to(device=device, dtype=dtype)
|
| 489 |
+
|
| 490 |
+
prior_cache: dict[tuple[Any, ...], torch.Tensor] = {}
|
| 491 |
+
sos = int(model.hp.start_speech_token)
|
| 492 |
+
mask = int(model.mask_token_id)
|
| 493 |
+
prior_prefix = "nvfp4.priors.block_"
|
| 494 |
+
for key in sorted(key for key in keys if key.startswith(prior_prefix)):
|
| 495 |
+
block_size = int(key.removeprefix(prior_prefix))
|
| 496 |
+
prior = handle.get_tensor(key)
|
| 497 |
+
if tuple(prior.shape) != (8_194,) or prior.dtype is not torch.float32:
|
| 498 |
+
raise RuntimeError(
|
| 499 |
+
f"Invalid cached prior {key}: {tuple(prior.shape)}/{prior.dtype}"
|
| 500 |
+
)
|
| 501 |
+
cache_key = (
|
| 502 |
+
block_size,
|
| 503 |
+
sos,
|
| 504 |
+
mask,
|
| 505 |
+
str(dtype),
|
| 506 |
+
str(device),
|
| 507 |
+
)
|
| 508 |
+
prior_cache[cache_key] = prior.to(device=device, non_blocking=False)
|
| 509 |
+
|
| 510 |
+
if drf_block_size not in {
|
| 511 |
+
int(key[0]) for key in prior_cache
|
| 512 |
+
}:
|
| 513 |
+
raise RuntimeError(
|
| 514 |
+
f"Artifact has no cached prior for D={drf_block_size}; source-free "
|
| 515 |
+
"models cannot compute a missing prior through the BF16 transformer"
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
meta_projection_parameters = [
|
| 519 |
+
name for name, parameter in model.named_parameters()
|
| 520 |
+
if parameter.is_meta and _is_projection_source_key(name)
|
| 521 |
+
]
|
| 522 |
+
unexpected_meta = [
|
| 523 |
+
name for name, parameter in model.named_parameters()
|
| 524 |
+
if parameter.is_meta and not _is_projection_source_key(name)
|
| 525 |
+
]
|
| 526 |
+
unexpected_meta.extend(
|
| 527 |
+
name for name, buffer in model.named_buffers() if buffer.is_meta
|
| 528 |
+
)
|
| 529 |
+
if len(meta_projection_parameters) != EXPECTED_LAYERS * 7:
|
| 530 |
+
raise RuntimeError(
|
| 531 |
+
f"Expected {EXPECTED_LAYERS * 7} meta projection sentinels, "
|
| 532 |
+
f"found {len(meta_projection_parameters)}"
|
| 533 |
+
)
|
| 534 |
+
if unexpected_meta:
|
| 535 |
+
raise RuntimeError(f"Unexpected meta model tensors: {unexpected_meta}")
|
| 536 |
+
|
| 537 |
+
model._uncond_block_prior_cache = prior_cache
|
| 538 |
+
model._nvfp4_source_free = True
|
| 539 |
+
model.eval()
|
| 540 |
+
return model
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
def load_prepacked_t3(
|
| 544 |
+
package: str | Path,
|
| 545 |
+
*,
|
| 546 |
+
device: torch.device | str = "cuda",
|
| 547 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 548 |
+
drf_block_size: int = 16,
|
| 549 |
+
autotune_cache: str | Path | None = None,
|
| 550 |
+
manifest_path: str | Path | None = None,
|
| 551 |
+
verify_sha256: bool = True,
|
| 552 |
+
):
|
| 553 |
+
"""Load a source-free T3 plus its reusable FlashInfer projection provider."""
|
| 554 |
+
if int(drf_block_size) not in SUPPORTED_RELEASE_BLOCK_SIZES:
|
| 555 |
+
raise RuntimeError(
|
| 556 |
+
f"Release profiles support D={SUPPORTED_RELEASE_BLOCK_SIZES}; "
|
| 557 |
+
f"D={drf_block_size} was not accepted for release use"
|
| 558 |
+
)
|
| 559 |
+
device = _canonical_cuda_device(device)
|
| 560 |
+
artifact_path, _config_path, manifest = _resolve_package(
|
| 561 |
+
package, manifest_path,
|
| 562 |
+
)
|
| 563 |
+
if verify_sha256:
|
| 564 |
+
expected_sha256 = manifest.get("artifact", {}).get("sha256")
|
| 565 |
+
actual_sha256 = _sha256_file(artifact_path)
|
| 566 |
+
if not expected_sha256 or actual_sha256 != expected_sha256:
|
| 567 |
+
raise RuntimeError(
|
| 568 |
+
f"Artifact SHA-256 mismatch: expected {expected_sha256}, "
|
| 569 |
+
f"found {actual_sha256}"
|
| 570 |
+
)
|
| 571 |
+
model = _load_source_free_model_state(
|
| 572 |
+
artifact_path,
|
| 573 |
+
device=device,
|
| 574 |
+
dtype=dtype,
|
| 575 |
+
drf_block_size=int(drf_block_size),
|
| 576 |
+
)
|
| 577 |
+
provider = CanonicalNVFP4ProjectionProvider(
|
| 578 |
+
artifact_path,
|
| 579 |
+
device=device,
|
| 580 |
+
dtype=dtype,
|
| 581 |
+
autotune_cache=autotune_cache,
|
| 582 |
+
)
|
| 583 |
+
model._flashinfer_projection_provider = provider
|
| 584 |
+
model._nvfp4_manifest = manifest
|
| 585 |
+
return model
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def _assemble_tts(
|
| 589 |
+
t3,
|
| 590 |
+
*,
|
| 591 |
+
ve_path: Path,
|
| 592 |
+
s3gen_path: Path,
|
| 593 |
+
tokenizer_path: Path,
|
| 594 |
+
device: torch.device,
|
| 595 |
+
):
|
| 596 |
+
from chatterbox.models.s3gen import S3Gen
|
| 597 |
+
from chatterbox.models.tokenizers import EnTokenizer
|
| 598 |
+
from chatterbox.models.voice_encoder import VoiceEncoder
|
| 599 |
+
from chatterbox_flash.tts import ChatterboxFlashTTS
|
| 600 |
+
|
| 601 |
+
ve = VoiceEncoder()
|
| 602 |
+
ve.load_state_dict(load_file(str(ve_path)))
|
| 603 |
+
ve.to(device).eval()
|
| 604 |
+
|
| 605 |
+
s3gen = S3Gen(meanflow=True)
|
| 606 |
+
s3gen.load_state_dict(load_file(str(s3gen_path)), strict=False)
|
| 607 |
+
s3gen.to(device).eval()
|
| 608 |
+
tokenizer = EnTokenizer(str(tokenizer_path))
|
| 609 |
+
return ChatterboxFlashTTS(t3, s3gen, ve, tokenizer, device)
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def load_prepacked_tts_local(
|
| 613 |
+
package: str | Path,
|
| 614 |
+
base_model_dir: str | Path,
|
| 615 |
+
*,
|
| 616 |
+
device: torch.device | str = "cuda",
|
| 617 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 618 |
+
drf_block_size: int = 16,
|
| 619 |
+
autotune_cache: str | Path | None = None,
|
| 620 |
+
verify_sha256: bool = True,
|
| 621 |
+
verify_base_sha256: bool = True,
|
| 622 |
+
):
|
| 623 |
+
"""Build the complete pipeline without loading ``t3_flash.safetensors``."""
|
| 624 |
+
device = _canonical_cuda_device(device)
|
| 625 |
+
base = Path(base_model_dir).expanduser().resolve()
|
| 626 |
+
t3 = load_prepacked_t3(
|
| 627 |
+
package,
|
| 628 |
+
device=device,
|
| 629 |
+
dtype=dtype,
|
| 630 |
+
drf_block_size=drf_block_size,
|
| 631 |
+
autotune_cache=autotune_cache,
|
| 632 |
+
verify_sha256=verify_sha256,
|
| 633 |
+
)
|
| 634 |
+
base_paths = {
|
| 635 |
+
filename: base / filename for filename in BASE_ASSET_FILENAMES
|
| 636 |
+
}
|
| 637 |
+
if verify_base_sha256:
|
| 638 |
+
_verify_base_assets(base_paths, t3._nvfp4_manifest)
|
| 639 |
+
return _assemble_tts(
|
| 640 |
+
t3,
|
| 641 |
+
ve_path=base_paths["ve.safetensors"],
|
| 642 |
+
s3gen_path=base_paths["s3gen.safetensors"],
|
| 643 |
+
tokenizer_path=base_paths["tokenizer.json"],
|
| 644 |
+
device=device,
|
| 645 |
+
)
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def load_prepacked_tts_from_hub(
|
| 649 |
+
repo_id: str,
|
| 650 |
+
*,
|
| 651 |
+
device: torch.device | str = "cuda",
|
| 652 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 653 |
+
drf_block_size: int = 16,
|
| 654 |
+
autotune_cache: str | Path | None = None,
|
| 655 |
+
revision: str | None = None,
|
| 656 |
+
base_repo_id: str = BASE_REPO_ID,
|
| 657 |
+
base_revision: str | None = BASE_CHECKPOINT_REVISION,
|
| 658 |
+
verify_sha256: bool = True,
|
| 659 |
+
verify_base_sha256: bool = True,
|
| 660 |
+
):
|
| 661 |
+
"""Download a derivative artifact plus unmodified base pipeline assets."""
|
| 662 |
+
from huggingface_hub import hf_hub_download
|
| 663 |
+
|
| 664 |
+
artifact_path = Path(hf_hub_download(
|
| 665 |
+
repo_id=repo_id,
|
| 666 |
+
filename=ARTIFACT_FILENAME,
|
| 667 |
+
revision=revision,
|
| 668 |
+
))
|
| 669 |
+
manifest_path = Path(hf_hub_download(
|
| 670 |
+
repo_id=repo_id,
|
| 671 |
+
filename=MANIFEST_FILENAME,
|
| 672 |
+
revision=revision,
|
| 673 |
+
))
|
| 674 |
+
base_paths = {
|
| 675 |
+
filename: Path(hf_hub_download(
|
| 676 |
+
repo_id=base_repo_id,
|
| 677 |
+
filename=filename,
|
| 678 |
+
revision=base_revision,
|
| 679 |
+
))
|
| 680 |
+
for filename in BASE_ASSET_FILENAMES
|
| 681 |
+
}
|
| 682 |
+
device = _canonical_cuda_device(device)
|
| 683 |
+
t3 = load_prepacked_t3(
|
| 684 |
+
artifact_path,
|
| 685 |
+
manifest_path=manifest_path,
|
| 686 |
+
device=device,
|
| 687 |
+
dtype=dtype,
|
| 688 |
+
drf_block_size=drf_block_size,
|
| 689 |
+
autotune_cache=autotune_cache,
|
| 690 |
+
verify_sha256=verify_sha256,
|
| 691 |
+
)
|
| 692 |
+
if verify_base_sha256:
|
| 693 |
+
_verify_base_assets(base_paths, t3._nvfp4_manifest)
|
| 694 |
+
return _assemble_tts(
|
| 695 |
+
t3,
|
| 696 |
+
ve_path=base_paths["ve.safetensors"],
|
| 697 |
+
s3gen_path=base_paths["s3gen.safetensors"],
|
| 698 |
+
tokenizer_path=base_paths["tokenizer.json"],
|
| 699 |
+
device=device,
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
__all__ = [
|
| 704 |
+
"CanonicalNVFP4ProjectionProvider",
|
| 705 |
+
"PreparedProjection",
|
| 706 |
+
"load_prepacked_t3",
|
| 707 |
+
"load_prepacked_tts_local",
|
| 708 |
+
"load_prepacked_tts_from_hub",
|
| 709 |
+
]
|
runtime/chatterbox-flash/chatterbox_flash/text_norm/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""English text normalization for Chatterbox-Flash inference.
|
| 2 |
+
|
| 3 |
+
This is a stripped-down port of Resemble's ``en_us_cleaner`` pipeline that
|
| 4 |
+
produces identical cleaned text but drops the ``SliceMap`` alignment plumbing
|
| 5 |
+
required for training. It expands numbers, currencies, units, time, phone
|
| 6 |
+
numbers and a small set of abbreviations so the input text matches the form
|
| 7 |
+
the model was trained on, which is essential for WER consistency on the
|
| 8 |
+
LibriSpeech-PC and Seed-TTS evaluation benchmarks.
|
| 9 |
+
|
| 10 |
+
The single entry point is :func:`en_us_cleaner`.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from .cleaners import en_us_cleaner
|
| 14 |
+
|
| 15 |
+
__all__ = ["en_us_cleaner"]
|
runtime/chatterbox-flash/chatterbox_flash/text_norm/cleaners.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``en_us_cleaner`` — exact-output port of Resemble's training-time text
|
| 2 |
+
normalization pipeline (number / time / phone / abbreviation expansion) with
|
| 3 |
+
the ``SliceMap`` alignment plumbing removed.
|
| 4 |
+
|
| 5 |
+
This pipeline runs at inference time before the speech tokenizer; matching the
|
| 6 |
+
training-time form is critical for WER on LibriSpeech-PC / Seed-TTS test-en.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
import unicodedata
|
| 13 |
+
|
| 14 |
+
from .number_norm import normalize_numbers
|
| 15 |
+
from .phone_norm import convert_phone_numbers_to_words
|
| 16 |
+
from .time_norm import convert_time_to_words
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
_whitespace_re = re.compile(r"\s+")
|
| 20 |
+
|
| 21 |
+
_abbreviations = [
|
| 22 |
+
(re.compile(r"\b%s\b\.?" % abbrev, re.IGNORECASE), expanded)
|
| 23 |
+
for abbrev, expanded in [
|
| 24 |
+
('mrs', 'misess'),
|
| 25 |
+
('mr', 'mister'),
|
| 26 |
+
('dr', 'doctor'),
|
| 27 |
+
('st', 'saint'),
|
| 28 |
+
('jr', 'junior'),
|
| 29 |
+
('maj', 'major'),
|
| 30 |
+
('gen', 'general'),
|
| 31 |
+
('drs', 'doctors'),
|
| 32 |
+
('rev', 'reverend'),
|
| 33 |
+
('lt', 'lieutenant'),
|
| 34 |
+
('hon', 'honorable'),
|
| 35 |
+
('sgt', 'sergeant'),
|
| 36 |
+
('capt', 'captain'),
|
| 37 |
+
('esq', 'esquire'),
|
| 38 |
+
('ltd', 'limited'),
|
| 39 |
+
('col', 'colonel'),
|
| 40 |
+
('ft', 'feet'),
|
| 41 |
+
('abbrev', 'abbreviation'),
|
| 42 |
+
('ave', 'avenue'),
|
| 43 |
+
('abstr', 'abstract'),
|
| 44 |
+
('addr', 'address'),
|
| 45 |
+
('jan', 'january'),
|
| 46 |
+
('feb', 'february'),
|
| 47 |
+
('mar', 'march'),
|
| 48 |
+
('apr', 'april'),
|
| 49 |
+
('jul', 'july'),
|
| 50 |
+
('aug', 'august'),
|
| 51 |
+
('sep', 'september'),
|
| 52 |
+
('sept', 'september'),
|
| 53 |
+
('oct', 'october'),
|
| 54 |
+
('nov', 'november'),
|
| 55 |
+
('dec', 'december'),
|
| 56 |
+
('mon', 'monday'),
|
| 57 |
+
('tue', 'tuesday'),
|
| 58 |
+
('wed', 'wednesday'),
|
| 59 |
+
('thur', 'thursday'),
|
| 60 |
+
('fri', 'friday'),
|
| 61 |
+
('sec', 'second'),
|
| 62 |
+
('min', 'minute'),
|
| 63 |
+
('mo', 'month'),
|
| 64 |
+
('yr', 'year'),
|
| 65 |
+
('cal', 'calorie'),
|
| 66 |
+
('dept', 'department'),
|
| 67 |
+
('gal', 'gallon'),
|
| 68 |
+
('kg', 'kilogram'),
|
| 69 |
+
('km', 'kilometer'),
|
| 70 |
+
('mt', 'mount'),
|
| 71 |
+
('oz', 'ounce'),
|
| 72 |
+
('vol', 'volume'),
|
| 73 |
+
('vs', 'versus'),
|
| 74 |
+
('yd', 'yard'),
|
| 75 |
+
(r'e\.g', 'eg'),
|
| 76 |
+
(r'i\.e', 'ie'),
|
| 77 |
+
('etc', 'etc'),
|
| 78 |
+
('ai', 'a i'),
|
| 79 |
+
('cms', 'c m s'),
|
| 80 |
+
('cdn', 'c d n'),
|
| 81 |
+
('ceo', 'c e o'),
|
| 82 |
+
('tts', 't t s'),
|
| 83 |
+
]
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
_custom_symbol_replacements = [
|
| 87 |
+
(re.compile('–'), '-'),
|
| 88 |
+
(re.compile('—'), '-'),
|
| 89 |
+
(re.compile(r'(?<=\d\s)>(?=\s\d)', re.IGNORECASE), 'is greater than'),
|
| 90 |
+
(re.compile(r'(?<=\d\s)<(?=\s\d)', re.IGNORECASE), 'is less than'),
|
| 91 |
+
(re.compile(r'(?<=\D)\s*-\s+(?=\D)'), ', '),
|
| 92 |
+
(re.compile(r'(?<=\D)\s+-\s*(?=\D)'), ', '),
|
| 93 |
+
(re.compile(r'(?<=[a-z])-(?=[a-z])', re.IGNORECASE), ' '),
|
| 94 |
+
(re.compile(r'(?<=[a-z])-(?=\d)', re.IGNORECASE), ' '),
|
| 95 |
+
(re.compile(r'(?<=\d)-(?=[a-z])', re.IGNORECASE), ' '),
|
| 96 |
+
(re.compile(r'(?<=\d)-(?=\d)', re.IGNORECASE), ' - '),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
_abbreviations += _custom_symbol_replacements
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _expand_abbreviations(text: str) -> str:
|
| 103 |
+
for regex, replacement in _abbreviations:
|
| 104 |
+
text = regex.sub(replacement, text)
|
| 105 |
+
return text
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _standardize_characters(text: str) -> str:
|
| 109 |
+
return unicodedata.normalize("NFKC", text)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _collapse_whitespace(text: str) -> str:
|
| 113 |
+
return _whitespace_re.sub(" ", text)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def en_us_cleaner(text: str) -> str:
|
| 117 |
+
"""English text normalization pipeline used for both training and the
|
| 118 |
+
paper's evaluations.
|
| 119 |
+
|
| 120 |
+
Matches the cleaned-text output of Resemble's ``transforms.text.cleaners.en_us_cleaner``
|
| 121 |
+
for any normal English input.
|
| 122 |
+
"""
|
| 123 |
+
text = _standardize_characters(text)
|
| 124 |
+
text = _collapse_whitespace(text)
|
| 125 |
+
text = _expand_abbreviations(text)
|
| 126 |
+
text = convert_time_to_words(text)
|
| 127 |
+
text = convert_phone_numbers_to_words(text)
|
| 128 |
+
text = normalize_numbers(text)
|
| 129 |
+
return text
|
runtime/chatterbox-flash/chatterbox_flash/text_norm/number_norm.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Number / currency / unit / year / ordinal expansion.
|
| 2 |
+
|
| 3 |
+
Verbatim logic ported from Resemble's training-time ``transforms.text.number_norm``
|
| 4 |
+
with the ``SliceMap`` alignment machinery stripped. The resulting cleaned text
|
| 5 |
+
is byte-identical to the original pipeline for any normal English input.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_comma_number_re = re.compile(r'(\(?[A-Z]{2,3})?([\$|£|¥|€|#|\(]*[0-9][0-9\,\.]+[0-9])([^\s]+)?')
|
| 14 |
+
_decimal_number_re = re.compile(r'(number\s)?([0-9]+\.[0-9]+)(\.|,|\?|!)?')
|
| 15 |
+
_hash_number_re = re.compile(r'(#)([0-9]+(?:\.[0-9]+)?)(\.|,|\?|!)?')
|
| 16 |
+
|
| 17 |
+
_pounds_re = re.compile(r'(\(?£)([0-9\.]*[0-9]+)(\.|,|\?|\!)?')
|
| 18 |
+
_yen_re = re.compile(r'(\(?¥)([0-9]+)(\.|,|\?|\!)?')
|
| 19 |
+
_euro_re = re.compile(r'(\(?€)([0-9\.]*[0-9]+)(\.|,|\?|\!)?')
|
| 20 |
+
_dollars_re = re.compile(
|
| 21 |
+
r'(?P<curr>\(?\$)'
|
| 22 |
+
r'(?P<val>[0-9,]*\.?[0-9]+)'
|
| 23 |
+
r'(?P<punc>[\.|,|\?|\!|\)]+)?'
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
_curr_abbrev_re = re.compile(
|
| 27 |
+
r'(?P<curr>\(?[$£¥€])'
|
| 28 |
+
r'(?P<val>[0-9]*\.?[0-9]+)'
|
| 29 |
+
r'(?:(?P<unit1>[BKMT])|(?:\s(?P<unit2>[BMbmTtr]+illion)))'
|
| 30 |
+
r'(?P<punc>[.,?!)]+)?'
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
_ml_re = re.compile(r'([0-9\.]*[0-9]+)(ml)(\.|,|\?|!)?')
|
| 34 |
+
_cl_re = re.compile(r'([0-9\.]*[0-9]+)(cl)(\.|,|\?|!)?')
|
| 35 |
+
_g_re = re.compile(r'([0-9\.]*[0-9]+)(g)(\.|,|\?|!)?')
|
| 36 |
+
_l_re = re.compile(r'([0-9\.]*[0-9]+)(l)(\.|,|\?|!)?')
|
| 37 |
+
_m_re = re.compile(r'([0-9\.]*[0-9]+)(m)(\.|,|\?|!)?')
|
| 38 |
+
_kg_re = re.compile(r'([0-9\.]*[0-9]+)(kg)(\.|,|\?|!)?')
|
| 39 |
+
_mm_re = re.compile(r'([0-9\.]*[0-9]+)(mm)(\.|,|\?|!)?')
|
| 40 |
+
_cm_re = re.compile(r'([0-9\.]*[0-9]+)(cm)(\.|,|\?|!)?')
|
| 41 |
+
_km_re = re.compile(r'([0-9\.]*[0-9]+)(km)(\.|,|\?|!)?')
|
| 42 |
+
_in_re = re.compile(r'([0-9\.]*[0-9]+)(in)(\.|,|\?|!)?')
|
| 43 |
+
_ft_re = re.compile(r'([0-9\.]*[0-9]+)(ft)(\.|,|\?|!)?')
|
| 44 |
+
_yd_re = re.compile(r'([0-9\.]*[0-9]+)(yd[s]?)(\.|,|\?|!)?')
|
| 45 |
+
_s_re = re.compile(r'([0-9\.]*[0-9]+)(s[ecs]*)(\.|,|\?|!)?')
|
| 46 |
+
_percent = re.compile(r'([0-9]{1,3}(?:,[0-9]{3})*(\.[0-9]+)?)(%)([.,?!])?')
|
| 47 |
+
|
| 48 |
+
_ordinal_re = re.compile(r'([0-9]+)(st|nd|rd|th)')
|
| 49 |
+
_number_re = re.compile(r'([0-9]+)(\.|,|\?|!)?')
|
| 50 |
+
_year_re = re.compile(
|
| 51 |
+
r'(to|years|year|from|after|before|by|until|in|since|around|circa|during)'
|
| 52 |
+
r'(\s)?'
|
| 53 |
+
r'(?<!\$|£|¥|€|₩|₹|₽|฿|₫|₦|₪|₱|₴)'
|
| 54 |
+
r'(1[1-9]|20)'
|
| 55 |
+
r'([0-9]{2})'
|
| 56 |
+
r'($|\-|\s\-|\D)',
|
| 57 |
+
re.IGNORECASE,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
_units = [
|
| 61 |
+
'', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
|
| 62 |
+
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
|
| 63 |
+
'seventeen', 'eighteen', 'nineteen',
|
| 64 |
+
]
|
| 65 |
+
_tens = [
|
| 66 |
+
'', 'ten', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy',
|
| 67 |
+
'eighty', 'ninety',
|
| 68 |
+
]
|
| 69 |
+
_digit_groups = ['', 'thousand', 'million', 'billion', 'trillion', 'quadrillion']
|
| 70 |
+
|
| 71 |
+
_ordinal_suffixes = [
|
| 72 |
+
('one', 'first'),
|
| 73 |
+
('two', 'second'),
|
| 74 |
+
('three', 'third'),
|
| 75 |
+
('five', 'fifth'),
|
| 76 |
+
('eight', 'eighth'),
|
| 77 |
+
('nine', 'ninth'),
|
| 78 |
+
('twelve', 'twelfth'),
|
| 79 |
+
('ty', 'tieth'),
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
_sub_ten_nums = {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09"}
|
| 83 |
+
|
| 84 |
+
_curr_dict = {"$": "dollars", "£": "pounds", "¥": "yen", "€": "euros"}
|
| 85 |
+
_unit_dict = {"B": "billion", "K": "thousand", "M": "million", "T": "trillion"}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def standard_number_to_words(n: int, digit_group: int) -> str:
|
| 89 |
+
parts = []
|
| 90 |
+
if n >= 1000:
|
| 91 |
+
parts.append(standard_number_to_words(n // 1000, digit_group + 1))
|
| 92 |
+
n = n % 1000
|
| 93 |
+
if n >= 100:
|
| 94 |
+
parts.append('%s hundred' % _units[n // 100])
|
| 95 |
+
if n % 100 >= len(_units):
|
| 96 |
+
parts.append(_tens[(n % 100) // 10])
|
| 97 |
+
parts.append(_units[(n % 100) % 10])
|
| 98 |
+
else:
|
| 99 |
+
parts.append(_units[n % 100])
|
| 100 |
+
if n > 0:
|
| 101 |
+
parts.append(_digit_groups[digit_group])
|
| 102 |
+
return ' '.join(x for x in parts if x)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _number_to_words(n: int) -> str:
|
| 106 |
+
if n >= 1000000000000000000:
|
| 107 |
+
return str(n)
|
| 108 |
+
if n == 0:
|
| 109 |
+
return 'zero'
|
| 110 |
+
if n % 100 == 0 and n % 1000 != 0 and n < 3000:
|
| 111 |
+
return standard_number_to_words(n // 100, 0) + ' hundred'
|
| 112 |
+
return standard_number_to_words(n, 0)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _remove_commas(match):
|
| 116 |
+
_, m1, m2 = match.groups(default="")
|
| 117 |
+
return ''.join([m1.replace(",", ""), m2])
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _expand_year(match):
|
| 121 |
+
prep, _, mill_cent, dec_year, post = match.groups(default="")
|
| 122 |
+
if mill_cent == "20" and dec_year in _sub_ten_nums:
|
| 123 |
+
year_out = mill_cent + dec_year
|
| 124 |
+
elif dec_year in _sub_ten_nums:
|
| 125 |
+
year = dec_year[-1]
|
| 126 |
+
year_out = mill_cent + " " + "oh" + " " + year
|
| 127 |
+
else:
|
| 128 |
+
year_out = _number_to_words(int(mill_cent)) + " " + _number_to_words(int(dec_year))
|
| 129 |
+
|
| 130 |
+
if "-" in post:
|
| 131 |
+
year_out += " to"
|
| 132 |
+
else:
|
| 133 |
+
year_out += post
|
| 134 |
+
return prep + " " + year_out
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _re_sub(pattern, repl, text: str) -> str:
|
| 138 |
+
"""Backwards (end-to-beginning) regex substitution, identical match order
|
| 139 |
+
to the training-time ``aligned_re_sub`` so non-overlapping match decisions
|
| 140 |
+
line up exactly."""
|
| 141 |
+
matches = sorted(re.finditer(pattern, text), key=lambda m: m.start(), reverse=True)
|
| 142 |
+
for m in matches:
|
| 143 |
+
middle = repl(m) if callable(repl) else repl
|
| 144 |
+
text = text[:m.start()] + middle + text[m.end():]
|
| 145 |
+
return text
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# --- helpers operating on (word, _) mapping list, but with the SliceMap dropped ---
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _to_word_list(text: str):
|
| 152 |
+
words = re.split(r"(\s+)", text)
|
| 153 |
+
return [[w] for w in words]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _from_word_list(mapping) -> str:
|
| 157 |
+
return ''.join(t[0] for t in mapping)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _convert_hash(mapping):
|
| 161 |
+
text = _from_word_list(mapping)
|
| 162 |
+
matches = re.findall(_hash_number_re, text)
|
| 163 |
+
for result in matches:
|
| 164 |
+
m = ''.join(result)
|
| 165 |
+
r_clean = ' '.join(["number", result[1]])
|
| 166 |
+
if len(result) == 3:
|
| 167 |
+
r_clean = r_clean + result[2]
|
| 168 |
+
for i, t in enumerate(mapping):
|
| 169 |
+
if m == t[0]:
|
| 170 |
+
mapping[i] = [r_clean]
|
| 171 |
+
return mapping
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def _expand_decimal_point(mapping):
|
| 175 |
+
text = _from_word_list(mapping)
|
| 176 |
+
matches = re.findall(_decimal_number_re, text)
|
| 177 |
+
for m in matches:
|
| 178 |
+
integral, fractional = m[1].split('.')
|
| 179 |
+
expanded_frac = ""
|
| 180 |
+
frac_it = iter(list(fractional))
|
| 181 |
+
for digit in frac_it:
|
| 182 |
+
if digit == "0":
|
| 183 |
+
expanded_frac += "zero "
|
| 184 |
+
else:
|
| 185 |
+
expanded_frac += digit + " "
|
| 186 |
+
for rest in frac_it:
|
| 187 |
+
expanded_frac += rest + " "
|
| 188 |
+
out = m[0] + integral + ' point ' + expanded_frac.strip()
|
| 189 |
+
if len(m) == 3:
|
| 190 |
+
out = out + m[2]
|
| 191 |
+
raw = ''.join(m)
|
| 192 |
+
for i, t in enumerate(mapping):
|
| 193 |
+
if t[0] == raw:
|
| 194 |
+
mapping[i] = [out]
|
| 195 |
+
return mapping
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _expand_dollars(mapping):
|
| 199 |
+
text = _from_word_list(mapping)
|
| 200 |
+
matches = re.findall(_dollars_re, text)
|
| 201 |
+
for m in matches:
|
| 202 |
+
val = m[1]
|
| 203 |
+
punc = m[2] if m[2] else ''
|
| 204 |
+
parts = val.split('.')
|
| 205 |
+
whole_num = int(parts[0]) if parts[0] else 0
|
| 206 |
+
fraction = int(parts[1]) if len(parts) > 1 and parts[1] else 0
|
| 207 |
+
dollar_unit = 'dollar' if whole_num == 1 else 'dollars'
|
| 208 |
+
cent_unit = 'cent' if fraction == 1 else 'cents'
|
| 209 |
+
if whole_num and fraction:
|
| 210 |
+
out = '%s %s, %s %s' % (whole_num, dollar_unit, fraction, cent_unit)
|
| 211 |
+
elif whole_num:
|
| 212 |
+
out = '%s %s' % (whole_num, dollar_unit)
|
| 213 |
+
elif fraction:
|
| 214 |
+
out = '%s %s' % (fraction, cent_unit)
|
| 215 |
+
else:
|
| 216 |
+
out = 'zero dollars'
|
| 217 |
+
if punc:
|
| 218 |
+
out = out + punc
|
| 219 |
+
raw = ''.join(m)
|
| 220 |
+
for i, t in enumerate(mapping):
|
| 221 |
+
if t[0] == raw:
|
| 222 |
+
mapping[i] = [out]
|
| 223 |
+
return mapping
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _expand_other_currency(mapping, regex, one, many):
|
| 227 |
+
text = _from_word_list(mapping)
|
| 228 |
+
matches = re.findall(regex, text)
|
| 229 |
+
for m in matches:
|
| 230 |
+
parts = m[1].split(".")
|
| 231 |
+
curr = one if int(parts[0]) == 1 else many
|
| 232 |
+
try:
|
| 233 |
+
out = parts[0] + " " + curr + " " + parts[1]
|
| 234 |
+
except IndexError:
|
| 235 |
+
out = parts[0] + " " + curr
|
| 236 |
+
raw = ''.join(m)
|
| 237 |
+
for i, t in enumerate(mapping):
|
| 238 |
+
if t[0] == raw:
|
| 239 |
+
mapping[i] = [out]
|
| 240 |
+
return mapping
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _expand_abbreviated_currency_unit(mapping):
|
| 244 |
+
text = _from_word_list(mapping)
|
| 245 |
+
matches = re.findall(_curr_abbrev_re, text)
|
| 246 |
+
to_remove = []
|
| 247 |
+
for m in matches:
|
| 248 |
+
curr = m[0]
|
| 249 |
+
val = m[1]
|
| 250 |
+
unit = m[2] if m[2] else m[3]
|
| 251 |
+
punc = m[4] if m[4] else ''
|
| 252 |
+
curr = curr.strip("(")
|
| 253 |
+
val_parts = val.split(".")
|
| 254 |
+
if len(val_parts) > 1:
|
| 255 |
+
val_out = val_parts[0] + ' ' + 'point ' + ' '.join(val_parts[1])
|
| 256 |
+
else:
|
| 257 |
+
val_out = val_parts[0]
|
| 258 |
+
try:
|
| 259 |
+
out = ' '.join([val_out, _unit_dict[unit], _curr_dict[curr]])
|
| 260 |
+
out = out + punc
|
| 261 |
+
raw = ''.join(m)
|
| 262 |
+
except KeyError:
|
| 263 |
+
out = ' '.join([val_out, unit, _curr_dict[curr]])
|
| 264 |
+
out = out + punc
|
| 265 |
+
raw = ''.join(m[:-2]) + " " + unit + punc
|
| 266 |
+
for i, t in enumerate(mapping):
|
| 267 |
+
if t[0] == raw:
|
| 268 |
+
mapping[i] = [out]
|
| 269 |
+
try:
|
| 270 |
+
join_text = ''.join([mapping[i][0], mapping[i + 1][0], mapping[i + 2][0]])
|
| 271 |
+
if join_text == raw:
|
| 272 |
+
mapping[i] = [out]
|
| 273 |
+
to_remove.append(i + 1)
|
| 274 |
+
to_remove.append(i + 2)
|
| 275 |
+
except IndexError:
|
| 276 |
+
continue
|
| 277 |
+
return [v for i, v in enumerate(mapping) if i not in to_remove]
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _expand_other_unit(mapping, regex, one, many):
|
| 281 |
+
text = _from_word_list(mapping)
|
| 282 |
+
matches = re.findall(regex, text)
|
| 283 |
+
for m in matches:
|
| 284 |
+
parts = re.split(r"\.", m[0])
|
| 285 |
+
unit = one if parts[0] == "1" else many
|
| 286 |
+
if len(parts) > 1:
|
| 287 |
+
dec = ''.join([i + " " for i in parts[1]])
|
| 288 |
+
parts = parts[0] + " " + "point" + " " + dec
|
| 289 |
+
unit = many
|
| 290 |
+
out = parts + unit
|
| 291 |
+
else:
|
| 292 |
+
out = parts[0] + " " + unit
|
| 293 |
+
out = out + m[-1]
|
| 294 |
+
raw = ''.join(m)
|
| 295 |
+
for i, t in enumerate(mapping):
|
| 296 |
+
if t[0] == raw:
|
| 297 |
+
mapping[i] = [out]
|
| 298 |
+
return mapping
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def _expand_ordinal(mapping):
|
| 302 |
+
text = _from_word_list(mapping)
|
| 303 |
+
matches = re.findall(_ordinal_re, text)
|
| 304 |
+
for m in matches:
|
| 305 |
+
num = _number_to_words(int(m[0]))
|
| 306 |
+
out = num + 'th'
|
| 307 |
+
for suffix, replacement in _ordinal_suffixes:
|
| 308 |
+
if num.endswith(suffix):
|
| 309 |
+
out = num[:-len(suffix)] + replacement
|
| 310 |
+
break
|
| 311 |
+
raw = ''.join(m)
|
| 312 |
+
for i, t in enumerate(mapping):
|
| 313 |
+
if t[0] == raw:
|
| 314 |
+
mapping[i] = [out]
|
| 315 |
+
return mapping
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _expand_number(mapping):
|
| 319 |
+
text = _from_word_list(mapping)
|
| 320 |
+
matches = re.findall(_number_re, text)
|
| 321 |
+
for m in matches:
|
| 322 |
+
out = _number_to_words(int(m[0])) + m[1]
|
| 323 |
+
raw = ''.join(m)
|
| 324 |
+
for i, t in enumerate(mapping):
|
| 325 |
+
text_re_nums = re.search(r"\d+", t[0])
|
| 326 |
+
if text_re_nums is None:
|
| 327 |
+
continue
|
| 328 |
+
if m[0] == text_re_nums.group():
|
| 329 |
+
rep = re.escape(raw)
|
| 330 |
+
j = re.sub(rep, out, t[0])
|
| 331 |
+
mapping[i] = [j]
|
| 332 |
+
return mapping
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def normalize_numbers(text: str) -> str:
|
| 336 |
+
text = _re_sub(_comma_number_re, _remove_commas, text)
|
| 337 |
+
# year norm is applied twice on purpose (matches training pipeline)
|
| 338 |
+
text = _re_sub(_year_re, _expand_year, text)
|
| 339 |
+
text = _re_sub(_year_re, _expand_year, text)
|
| 340 |
+
|
| 341 |
+
mapping = _to_word_list(text)
|
| 342 |
+
mapping = _expand_abbreviated_currency_unit(mapping)
|
| 343 |
+
mapping = _expand_other_currency(mapping, _pounds_re, "pound", "pounds")
|
| 344 |
+
mapping = _expand_other_currency(mapping, _yen_re, "yen", "yen")
|
| 345 |
+
mapping = _expand_other_currency(mapping, _euro_re, "euro", "euros")
|
| 346 |
+
mapping = _expand_other_unit(mapping, _ml_re, "milliliter", "milliliters")
|
| 347 |
+
mapping = _expand_other_unit(mapping, _cl_re, "centiliter", "centiliters")
|
| 348 |
+
mapping = _expand_other_unit(mapping, _g_re, "gram", "grams")
|
| 349 |
+
mapping = _expand_other_unit(mapping, _kg_re, "kilogram", "kilograms")
|
| 350 |
+
mapping = _expand_other_unit(mapping, _mm_re, "millimeter", "millimeters")
|
| 351 |
+
mapping = _expand_other_unit(mapping, _cm_re, "centimeter", "centimeters")
|
| 352 |
+
mapping = _expand_other_unit(mapping, _km_re, "kilometer", "kilometers")
|
| 353 |
+
mapping = _expand_other_unit(mapping, _in_re, "inch", "inches")
|
| 354 |
+
mapping = _expand_other_unit(mapping, _ft_re, "foot", "feet")
|
| 355 |
+
mapping = _expand_other_unit(mapping, _l_re, "liter", "liters")
|
| 356 |
+
mapping = _expand_other_unit(mapping, _m_re, "meter", "meters")
|
| 357 |
+
mapping = _expand_other_unit(mapping, _yd_re, "yard", "yards")
|
| 358 |
+
mapping = _expand_other_unit(mapping, _s_re, "second", "seconds")
|
| 359 |
+
mapping = _expand_other_unit(mapping, _percent, "%", "percent")
|
| 360 |
+
mapping = _expand_dollars(mapping)
|
| 361 |
+
mapping = _convert_hash(mapping)
|
| 362 |
+
mapping = _expand_decimal_point(mapping)
|
| 363 |
+
mapping = _expand_ordinal(mapping)
|
| 364 |
+
mapping = _expand_number(mapping)
|
| 365 |
+
|
| 366 |
+
return _from_word_list(mapping)
|
runtime/chatterbox-flash/chatterbox_flash/text_norm/phone_norm.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phone number expansion (e.g. "555-1234" -> "five five five, one two three four")
|
| 2 |
+
ported from ``transforms.text.phone_num_norm`` without SliceMap."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_NUM_WORDS = {
|
| 10 |
+
0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four',
|
| 11 |
+
5: 'five', 6: 'six', 7: 'seven', 8: 'eight', 9: 'nine',
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
_NON_ALPHA = re.compile(r"(\s?[^a-zA-Z]+\s?)")
|
| 15 |
+
_PHONE_NUM_CHARS = set(list("0123456789.+-()[] "))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def is_phone_num(text: str) -> bool:
|
| 19 |
+
text = text.rstrip('.!?,;')
|
| 20 |
+
if text.count(".") == 1:
|
| 21 |
+
return False
|
| 22 |
+
if any(c.isalpha() for c in text):
|
| 23 |
+
return False
|
| 24 |
+
if any(c not in _PHONE_NUM_CHARS for c in text):
|
| 25 |
+
return False
|
| 26 |
+
nums = [c for c in text if c.isdigit()]
|
| 27 |
+
if not (5 < len(nums) < 14):
|
| 28 |
+
return False
|
| 29 |
+
if "+" not in text and "." not in text and list(text).count("-") == 1:
|
| 30 |
+
nums = [[c for c in subtext if c.isdigit()] for subtext in text.split("-")]
|
| 31 |
+
if len(nums) == 2 and all(
|
| 32 |
+
subnums[0] in ("1", "2") and len(subnums) == 4 for subnums in nums
|
| 33 |
+
):
|
| 34 |
+
return False
|
| 35 |
+
return True
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _group_num(text):
|
| 39 |
+
prev_was_digit = False
|
| 40 |
+
group = []
|
| 41 |
+
num_groups = []
|
| 42 |
+
for c in text:
|
| 43 |
+
if c.isdigit():
|
| 44 |
+
if len(group) == 0 or prev_was_digit:
|
| 45 |
+
group.append(c)
|
| 46 |
+
else:
|
| 47 |
+
num_groups.append(group)
|
| 48 |
+
group = [c]
|
| 49 |
+
prev_was_digit = True
|
| 50 |
+
else:
|
| 51 |
+
prev_was_digit = False
|
| 52 |
+
if group:
|
| 53 |
+
num_groups.append(group)
|
| 54 |
+
return num_groups
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _natural_phone_num_grouping(nums, max_natural_group=4):
|
| 58 |
+
if len(nums) == 0:
|
| 59 |
+
return []
|
| 60 |
+
if len(nums) <= max_natural_group:
|
| 61 |
+
return [nums]
|
| 62 |
+
if len(nums) == 5:
|
| 63 |
+
return [nums[:2], nums[2:]]
|
| 64 |
+
if len(nums) == 8:
|
| 65 |
+
return [nums[:2], nums[2:5], nums[5:]]
|
| 66 |
+
return _natural_phone_num_grouping(nums[:-max_natural_group], 3) + [nums[-max_natural_group:]]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _convert_phone_num_to_words(text: str) -> str:
|
| 70 |
+
original_punctuation = ''
|
| 71 |
+
while text and text[-1] in '.!?,;':
|
| 72 |
+
original_punctuation = text[-1] + original_punctuation
|
| 73 |
+
text = text[:-1]
|
| 74 |
+
|
| 75 |
+
num_groups = _group_num(text)
|
| 76 |
+
num_word_groups = [
|
| 77 |
+
[" " + _NUM_WORDS[int(c)] for c in nums if c.isdigit()] for nums in num_groups
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
output = []
|
| 81 |
+
for num_word_group in reversed(num_word_groups):
|
| 82 |
+
grouplen = len(num_word_group)
|
| 83 |
+
regroupped = [num_word_group]
|
| 84 |
+
if grouplen > 4:
|
| 85 |
+
regroupped = reversed(_natural_phone_num_grouping(num_word_group))
|
| 86 |
+
for new_group in regroupped:
|
| 87 |
+
output = ["".join(new_group)] + output
|
| 88 |
+
|
| 89 |
+
output = ",".join(output).strip()
|
| 90 |
+
parts = re.split(r"\s", text)
|
| 91 |
+
output = (" " if parts[0] == "" else "") + output + (" " if parts[-1] == "" else "")
|
| 92 |
+
output += original_punctuation
|
| 93 |
+
return output
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _convert_phone_num_match_to_words(match: re.Match) -> str:
|
| 97 |
+
text = match.group()
|
| 98 |
+
if is_phone_num(text):
|
| 99 |
+
return _convert_phone_num_to_words(text)
|
| 100 |
+
return text
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def convert_phone_numbers_to_words(text: str) -> str:
|
| 104 |
+
matches = sorted(_NON_ALPHA.finditer(text), key=lambda m: m.start(), reverse=True)
|
| 105 |
+
for m in matches:
|
| 106 |
+
text = text[: m.start()] + _convert_phone_num_match_to_words(m) + text[m.end():]
|
| 107 |
+
return text
|
runtime/chatterbox-flash/chatterbox_flash/text_norm/time_norm.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Time-of-day expansion (12:30, 3.45 pm, ...) ported from
|
| 2 |
+
``transforms.text.time_norm`` without the SliceMap return value."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
from .number_norm import standard_number_to_words
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
_time_pattern = re.compile(
|
| 12 |
+
r'(?P<hour>\d{1,2})(?P<delim>[\.\:])?\s*(?P<min>\d{2})?\s*(?P<am_pm>[AaPp]\.?[Mm]\.?)?'
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _time_to_words(match: re.Match) -> str:
|
| 17 |
+
gd = match.groupdict()
|
| 18 |
+
hours = int(gd['hour'])
|
| 19 |
+
delim = gd.get("delim")
|
| 20 |
+
minutes = gd.get("min")
|
| 21 |
+
am_pm = gd.get("am_pm")
|
| 22 |
+
|
| 23 |
+
has_minutes = minutes is not None
|
| 24 |
+
has_am_pm = am_pm is not None
|
| 25 |
+
is_time = has_am_pm or (delim == ":" and has_minutes) or (delim == "." and has_am_pm)
|
| 26 |
+
if not is_time:
|
| 27 |
+
return match.group()
|
| 28 |
+
|
| 29 |
+
minutes_i = int(minutes) if has_minutes else 0
|
| 30 |
+
am_pm = (' ' + am_pm.replace('.', '').strip().upper()) if has_am_pm else ''
|
| 31 |
+
hour_word = standard_number_to_words(hours, 0)
|
| 32 |
+
|
| 33 |
+
if minutes_i < 10 and minutes_i != 0:
|
| 34 |
+
minute_word = standard_number_to_words(minutes_i, 0)
|
| 35 |
+
minute_word = f"oh {minute_word}"
|
| 36 |
+
else:
|
| 37 |
+
minute_word = standard_number_to_words(minutes_i, 0) if minutes_i > 0 else ""
|
| 38 |
+
|
| 39 |
+
if minutes_i > 0:
|
| 40 |
+
minute_word = f" {minute_word}"
|
| 41 |
+
|
| 42 |
+
return f"{hour_word}{minute_word}{am_pm}"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def convert_time_to_words(text: str) -> str:
|
| 46 |
+
matches = sorted(_time_pattern.finditer(text), key=lambda m: m.start(), reverse=True)
|
| 47 |
+
for m in matches:
|
| 48 |
+
text = text[: m.start()] + _time_to_words(m) + text[m.end():]
|
| 49 |
+
return text
|
runtime/chatterbox-flash/chatterbox_flash/tts.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User-facing API for Chatterbox-Flash."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import dataclasses
|
| 6 |
+
import logging
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Iterable, Literal
|
| 10 |
+
|
| 11 |
+
import librosa
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
from huggingface_hub import hf_hub_download
|
| 16 |
+
from safetensors.torch import load_file
|
| 17 |
+
|
| 18 |
+
from chatterbox.models.s3gen import S3GEN_SR, S3Gen
|
| 19 |
+
from chatterbox.models.s3tokenizer import S3_SR
|
| 20 |
+
from chatterbox.models.t3.modules.cond_enc import T3Cond
|
| 21 |
+
from chatterbox.models.tokenizers import EnTokenizer
|
| 22 |
+
from chatterbox.models.voice_encoder import VoiceEncoder
|
| 23 |
+
|
| 24 |
+
from .model import ChatterboxFlashT3
|
| 25 |
+
from .text_norm import en_us_cleaner
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
DEFAULT_REPO_ID = "ResembleAI/chatterbox-flash"
|
| 30 |
+
|
| 31 |
+
# Per-file names expected inside a checkpoint dir / HF repo.
|
| 32 |
+
T3_FILENAME = "t3_flash.safetensors"
|
| 33 |
+
S3GEN_FILENAME = "s3gen.safetensors"
|
| 34 |
+
VE_FILENAME = "ve.safetensors"
|
| 35 |
+
TOKENIZER_FILENAME = "tokenizer.json"
|
| 36 |
+
|
| 37 |
+
# Conditioning durations (mirrored from chatterbox-tts).
|
| 38 |
+
ENC_COND_LEN = 6 * S3_SR
|
| 39 |
+
DEC_COND_LEN = 10 * S3GEN_SR
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class Conditionals:
|
| 44 |
+
"""Prepared reference-speech conditioning for one speaker.
|
| 45 |
+
|
| 46 |
+
Bundles the T3 conditioning (speaker embedding + prompt speech tokens)
|
| 47 |
+
and the S3Gen reference dict produced by :meth:`S3Gen.embed_ref`. Use
|
| 48 |
+
:meth:`save` / :meth:`load` to cache these for repeated generations.
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
t3: T3Cond
|
| 52 |
+
gen: dict
|
| 53 |
+
|
| 54 |
+
def to(self, device) -> "Conditionals":
|
| 55 |
+
self.t3 = self.t3.to(device=device)
|
| 56 |
+
for k, v in self.gen.items():
|
| 57 |
+
if torch.is_tensor(v):
|
| 58 |
+
self.gen[k] = v.to(device=device)
|
| 59 |
+
return self
|
| 60 |
+
|
| 61 |
+
def save(self, fpath: Path) -> None:
|
| 62 |
+
torch.save({"t3": self.t3.__dict__, "gen": self.gen}, fpath)
|
| 63 |
+
|
| 64 |
+
@classmethod
|
| 65 |
+
def load(cls, fpath, map_location="cpu") -> "Conditionals":
|
| 66 |
+
if isinstance(map_location, str):
|
| 67 |
+
map_location = torch.device(map_location)
|
| 68 |
+
kwargs = torch.load(fpath, map_location=map_location, weights_only=True)
|
| 69 |
+
return cls(T3Cond(**kwargs["t3"]), kwargs["gen"])
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _speech_len_for_text_tokens(n_text_tokens: int) -> int:
|
| 73 |
+
"""Heuristic max-speech-token budget per utterance (matches paper config)."""
|
| 74 |
+
return max(n_text_tokens * 6, 300)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class ChatterboxFlashTTS:
|
| 78 |
+
"""Zero-shot TTS pipeline matching the configuration in the paper.
|
| 79 |
+
|
| 80 |
+
Stage 1 is the :class:`ChatterboxFlashT3` block-diffusion decoder.
|
| 81 |
+
Stage 2 is the unmodified Chatterbox-TTS S3Gen flow-matching vocoder.
|
| 82 |
+
Reference encoding uses the GE2E-trained voice encoder from Chatterbox.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(
|
| 86 |
+
self,
|
| 87 |
+
t3: ChatterboxFlashT3,
|
| 88 |
+
s3gen: S3Gen,
|
| 89 |
+
ve: VoiceEncoder,
|
| 90 |
+
tokenizer: EnTokenizer,
|
| 91 |
+
device: str | torch.device,
|
| 92 |
+
conds: Conditionals | None = None,
|
| 93 |
+
) -> None:
|
| 94 |
+
self.sr = S3GEN_SR
|
| 95 |
+
self.t3 = t3
|
| 96 |
+
self.s3gen = s3gen
|
| 97 |
+
self.ve = ve
|
| 98 |
+
self.tokenizer = tokenizer
|
| 99 |
+
self.device = torch.device(device) if isinstance(device, str) else device
|
| 100 |
+
self.conds = conds
|
| 101 |
+
|
| 102 |
+
# -------- construction ------------------------------------------------
|
| 103 |
+
|
| 104 |
+
@classmethod
|
| 105 |
+
def from_local(
|
| 106 |
+
cls,
|
| 107 |
+
ckpt_dir: str | Path,
|
| 108 |
+
device: str | torch.device,
|
| 109 |
+
*,
|
| 110 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 111 |
+
drf_block_size: int = 16,
|
| 112 |
+
) -> "ChatterboxFlashTTS":
|
| 113 |
+
"""Load a Chatterbox-Flash checkpoint from a local directory.
|
| 114 |
+
|
| 115 |
+
Expects ``t3_flash.safetensors`` (with the expanded MASK row),
|
| 116 |
+
``s3gen.safetensors``, ``ve.safetensors`` and ``tokenizer.json``.
|
| 117 |
+
"""
|
| 118 |
+
ckpt_dir = Path(ckpt_dir)
|
| 119 |
+
device = torch.device(device) if isinstance(device, str) else device
|
| 120 |
+
|
| 121 |
+
ve = VoiceEncoder()
|
| 122 |
+
ve.load_state_dict(load_file(str(ckpt_dir / VE_FILENAME)))
|
| 123 |
+
ve.to(device).eval()
|
| 124 |
+
|
| 125 |
+
t3 = ChatterboxFlashT3(drf_block_size=drf_block_size)
|
| 126 |
+
t3_state = load_file(str(ckpt_dir / T3_FILENAME))
|
| 127 |
+
if "model" in t3_state: # legacy layout
|
| 128 |
+
t3_state = t3_state["model"][0]
|
| 129 |
+
t3.load_state_dict(t3_state)
|
| 130 |
+
t3.to(device=device, dtype=dtype).eval()
|
| 131 |
+
# Prime the PMI unconditional block prior now (at load) so the first
|
| 132 |
+
# generate() doesn't fold the one-shot prior forward into the timed /
|
| 133 |
+
# RTF loop. A generate() with a different block size recomputes it
|
| 134 |
+
# automatically (cache keyed on block_size/dtype/device).
|
| 135 |
+
t3.prime_uncond_block_prior(dtype, device)
|
| 136 |
+
|
| 137 |
+
# ``meanflow=True``: the released ``s3gen.safetensors`` is the
|
| 138 |
+
# meanflow-distilled vocoder, sampled with the meanflow ODE path at 2
|
| 139 |
+
# CFM steps (matches the internal ``S3GenStreamer`` defaults). On a
|
| 140 |
+
# standard-CFM checkpoint this would degrade quality — flip to False.
|
| 141 |
+
s3gen = S3Gen(meanflow=True)
|
| 142 |
+
s3gen.load_state_dict(
|
| 143 |
+
load_file(str(ckpt_dir / S3GEN_FILENAME)), strict=False,
|
| 144 |
+
)
|
| 145 |
+
s3gen.to(device).eval()
|
| 146 |
+
|
| 147 |
+
tokenizer = EnTokenizer(str(ckpt_dir / TOKENIZER_FILENAME))
|
| 148 |
+
|
| 149 |
+
return cls(t3, s3gen, ve, tokenizer, device)
|
| 150 |
+
|
| 151 |
+
@classmethod
|
| 152 |
+
def from_pretrained(
|
| 153 |
+
cls,
|
| 154 |
+
repo_id: str = DEFAULT_REPO_ID,
|
| 155 |
+
device: str | torch.device = "cuda",
|
| 156 |
+
*,
|
| 157 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 158 |
+
drf_block_size: int = 16,
|
| 159 |
+
) -> "ChatterboxFlashTTS":
|
| 160 |
+
"""Fetch a Chatterbox-Flash release from the Hugging Face Hub."""
|
| 161 |
+
last = None
|
| 162 |
+
for fname in (
|
| 163 |
+
VE_FILENAME, T3_FILENAME, S3GEN_FILENAME, TOKENIZER_FILENAME,
|
| 164 |
+
):
|
| 165 |
+
last = hf_hub_download(repo_id=repo_id, filename=fname)
|
| 166 |
+
return cls.from_local(
|
| 167 |
+
Path(last).parent, device,
|
| 168 |
+
dtype=dtype, drf_block_size=drf_block_size,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# -------- conditioning -----------------------------------------------
|
| 172 |
+
|
| 173 |
+
def prepare_conditionals(
|
| 174 |
+
self,
|
| 175 |
+
wav_fpath: str | Path,
|
| 176 |
+
exaggeration: float = 0.5,
|
| 177 |
+
) -> Conditionals:
|
| 178 |
+
"""Build the speaker / prompt-speech conditioning for a reference wav.
|
| 179 |
+
|
| 180 |
+
The reference is resampled and segmented identically to chatterbox-tts:
|
| 181 |
+
the first 6 s feed S3's speech tokenizer to produce prompt speech
|
| 182 |
+
tokens, the first 10 s feed S3Gen's :meth:`embed_ref`, and the GE2E
|
| 183 |
+
voice encoder produces the global speaker embedding.
|
| 184 |
+
"""
|
| 185 |
+
s3gen_ref_wav, _sr = librosa.load(str(wav_fpath), sr=S3GEN_SR)
|
| 186 |
+
ref_16k_wav = librosa.resample(s3gen_ref_wav, orig_sr=S3GEN_SR, target_sr=S3_SR)
|
| 187 |
+
|
| 188 |
+
s3gen_ref_wav = s3gen_ref_wav[:DEC_COND_LEN]
|
| 189 |
+
s3gen_ref_dict = self.s3gen.embed_ref(
|
| 190 |
+
s3gen_ref_wav, S3GEN_SR, device=self.device,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
t3_cond_prompt_tokens = None
|
| 194 |
+
if plen := self.t3.hp.speech_cond_prompt_len:
|
| 195 |
+
tokens, _ = self.s3gen.tokenizer.forward(
|
| 196 |
+
[ref_16k_wav[:ENC_COND_LEN]], max_len=plen,
|
| 197 |
+
)
|
| 198 |
+
t3_cond_prompt_tokens = torch.atleast_2d(tokens).to(self.device)
|
| 199 |
+
|
| 200 |
+
t3_dtype = next(self.t3.parameters()).dtype
|
| 201 |
+
ve_embed = torch.from_numpy(
|
| 202 |
+
self.ve.embeds_from_wavs([ref_16k_wav], sample_rate=S3_SR),
|
| 203 |
+
)
|
| 204 |
+
ve_embed = ve_embed.mean(axis=0, keepdim=True).to(device=self.device, dtype=t3_dtype)
|
| 205 |
+
|
| 206 |
+
t3_cond = T3Cond(
|
| 207 |
+
speaker_emb=ve_embed,
|
| 208 |
+
cond_prompt_speech_tokens=t3_cond_prompt_tokens,
|
| 209 |
+
emotion_adv=exaggeration * torch.ones(1, 1, 1, dtype=t3_dtype),
|
| 210 |
+
).to(device=self.device)
|
| 211 |
+
self.conds = Conditionals(t3_cond, s3gen_ref_dict)
|
| 212 |
+
return self.conds
|
| 213 |
+
|
| 214 |
+
# -------- tokenization -----------------------------------------------
|
| 215 |
+
|
| 216 |
+
def _encode_text(
|
| 217 |
+
self, text: str, normalize_text: bool = True,
|
| 218 |
+
) -> torch.Tensor:
|
| 219 |
+
if normalize_text:
|
| 220 |
+
text = en_us_cleaner(text)
|
| 221 |
+
text_tokens = self.tokenizer.text_to_tokens(text).to(self.device)
|
| 222 |
+
text_tokens = F.pad(text_tokens, (1, 0), value=self.t3.hp.start_text_token)
|
| 223 |
+
text_tokens = F.pad(text_tokens, (0, 1), value=self.t3.hp.stop_text_token)
|
| 224 |
+
return text_tokens
|
| 225 |
+
|
| 226 |
+
# -------- inference --------------------------------------------------
|
| 227 |
+
|
| 228 |
+
@torch.inference_mode()
|
| 229 |
+
def generate(
|
| 230 |
+
self,
|
| 231 |
+
text: str,
|
| 232 |
+
*,
|
| 233 |
+
audio_prompt_path: str | Path | None = None,
|
| 234 |
+
exaggeration: float = 0.5,
|
| 235 |
+
normalize_text: bool = True,
|
| 236 |
+
num_steps: int = 10,
|
| 237 |
+
temperature: float = 0.6,
|
| 238 |
+
time_shift_tau: float = 0.1,
|
| 239 |
+
omnivoice_schedule_t_shift: float = 0.5,
|
| 240 |
+
cfg_scale: float = 1.0,
|
| 241 |
+
position_temperature: float = 5.0,
|
| 242 |
+
pmi_uncond_prior_precompute: bool = True,
|
| 243 |
+
use_cuda_graph: bool = True,
|
| 244 |
+
backend: Literal["auto", "flashinfer", "torch", "mlx"] = "auto",
|
| 245 |
+
max_speech_tokens: int | None = None,
|
| 246 |
+
n_cfm_timesteps: int = 2,
|
| 247 |
+
) -> torch.Tensor:
|
| 248 |
+
"""Synthesize a single utterance.
|
| 249 |
+
|
| 250 |
+
Parameters mirror the inference defaults from the paper (best
|
| 251 |
+
configuration). Returns the synthesized waveform as a 1xT float
|
| 252 |
+
tensor at :attr:`sr` Hz. See
|
| 253 |
+
:meth:`ChatterboxFlashT3.generate` for the full parameter spec.
|
| 254 |
+
"""
|
| 255 |
+
if audio_prompt_path is not None:
|
| 256 |
+
self.prepare_conditionals(audio_prompt_path, exaggeration=exaggeration)
|
| 257 |
+
if self.conds is None:
|
| 258 |
+
raise ValueError(
|
| 259 |
+
"Conditioning not prepared — pass audio_prompt_path or call "
|
| 260 |
+
"prepare_conditionals() first.",
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
text_tokens = self._encode_text(text, normalize_text=normalize_text)
|
| 264 |
+
n_tokens = int(text_tokens.size(1))
|
| 265 |
+
total_speech_len = _speech_len_for_text_tokens(n_tokens)
|
| 266 |
+
if max_speech_tokens is not None:
|
| 267 |
+
total_speech_len = min(total_speech_len, max_speech_tokens)
|
| 268 |
+
|
| 269 |
+
speech_tokens = self.t3.generate(
|
| 270 |
+
t3_cond=self.conds.t3,
|
| 271 |
+
text_tokens=text_tokens,
|
| 272 |
+
text_token_lens=torch.tensor([n_tokens], device=self.device),
|
| 273 |
+
total_speech_len=total_speech_len,
|
| 274 |
+
num_steps=num_steps,
|
| 275 |
+
temperature=temperature,
|
| 276 |
+
time_shift_tau=time_shift_tau,
|
| 277 |
+
omnivoice_schedule_t_shift=omnivoice_schedule_t_shift,
|
| 278 |
+
cfg_scale=cfg_scale,
|
| 279 |
+
position_temperature=position_temperature,
|
| 280 |
+
pmi_uncond_prior_precompute=pmi_uncond_prior_precompute,
|
| 281 |
+
use_cuda_graph=use_cuda_graph,
|
| 282 |
+
backend=backend,
|
| 283 |
+
batch_size=1,
|
| 284 |
+
)
|
| 285 |
+
speech_tokens = self._trim_to_eos(speech_tokens[0])
|
| 286 |
+
wav, _ = self.s3gen.inference(
|
| 287 |
+
speech_tokens=speech_tokens.to(self.device),
|
| 288 |
+
ref_dict=self.conds.gen,
|
| 289 |
+
n_cfm_timesteps=n_cfm_timesteps,
|
| 290 |
+
)
|
| 291 |
+
return wav.detach().cpu().squeeze(0)
|
| 292 |
+
|
| 293 |
+
@torch.inference_mode()
|
| 294 |
+
def generate_batch(
|
| 295 |
+
self,
|
| 296 |
+
texts: Iterable[str],
|
| 297 |
+
*,
|
| 298 |
+
audio_prompt_paths: Iterable[str | Path] | None = None,
|
| 299 |
+
conds_list: Iterable[Conditionals] | None = None,
|
| 300 |
+
exaggeration: float = 0.5,
|
| 301 |
+
normalize_text: bool = True,
|
| 302 |
+
num_steps: int = 10,
|
| 303 |
+
temperature: float = 0.6,
|
| 304 |
+
time_shift_tau: float = 0.1,
|
| 305 |
+
omnivoice_schedule_t_shift: float = 0.5,
|
| 306 |
+
cfg_scale: float = 1.0,
|
| 307 |
+
position_temperature: float = 5.0,
|
| 308 |
+
pmi_uncond_prior_precompute: bool = True,
|
| 309 |
+
use_cuda_graph: bool = True,
|
| 310 |
+
backend: Literal["auto", "flashinfer", "torch", "mlx"] = "auto",
|
| 311 |
+
max_speech_tokens: int | None = None,
|
| 312 |
+
n_cfm_timesteps: int = 2,
|
| 313 |
+
) -> list[torch.Tensor]:
|
| 314 |
+
"""Synthesize a heterogeneous batch (one prompt + text per row).
|
| 315 |
+
|
| 316 |
+
Exactly one of ``audio_prompt_paths`` and ``conds_list`` must be
|
| 317 |
+
provided. Both ``texts`` and the conditioning iterable must have the
|
| 318 |
+
same length. Returns a list of 1D waveforms (one per row).
|
| 319 |
+
"""
|
| 320 |
+
texts = list(texts)
|
| 321 |
+
if audio_prompt_paths is not None and conds_list is not None:
|
| 322 |
+
raise ValueError("Pass exactly one of audio_prompt_paths / conds_list.")
|
| 323 |
+
if audio_prompt_paths is None and conds_list is None:
|
| 324 |
+
raise ValueError("Pass either audio_prompt_paths or conds_list.")
|
| 325 |
+
|
| 326 |
+
if conds_list is None:
|
| 327 |
+
conds_list = [
|
| 328 |
+
dataclasses.replace(
|
| 329 |
+
self.prepare_conditionals(p, exaggeration=exaggeration),
|
| 330 |
+
) if False else self.prepare_conditionals(p, exaggeration=exaggeration)
|
| 331 |
+
for p in audio_prompt_paths
|
| 332 |
+
]
|
| 333 |
+
else:
|
| 334 |
+
conds_list = list(conds_list)
|
| 335 |
+
if len(conds_list) != len(texts):
|
| 336 |
+
raise ValueError(
|
| 337 |
+
f"texts ({len(texts)}) and conditioning ({len(conds_list)}) "
|
| 338 |
+
"must have equal length.",
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
text_tokens_list: list[torch.Tensor] = []
|
| 342 |
+
speech_lens: list[int] = []
|
| 343 |
+
for t in texts:
|
| 344 |
+
tt = self._encode_text(t, normalize_text=normalize_text)
|
| 345 |
+
text_tokens_list.append(tt)
|
| 346 |
+
sl = _speech_len_for_text_tokens(int(tt.size(1)))
|
| 347 |
+
if max_speech_tokens is not None:
|
| 348 |
+
sl = min(sl, max_speech_tokens)
|
| 349 |
+
speech_lens.append(sl)
|
| 350 |
+
|
| 351 |
+
speech_tokens_batch = self.t3.generate(
|
| 352 |
+
t3_cond=[c.t3 for c in conds_list],
|
| 353 |
+
text_tokens=text_tokens_list,
|
| 354 |
+
text_token_lens=None,
|
| 355 |
+
total_speech_len=speech_lens,
|
| 356 |
+
num_steps=num_steps,
|
| 357 |
+
temperature=temperature,
|
| 358 |
+
time_shift_tau=time_shift_tau,
|
| 359 |
+
omnivoice_schedule_t_shift=omnivoice_schedule_t_shift,
|
| 360 |
+
cfg_scale=cfg_scale,
|
| 361 |
+
position_temperature=position_temperature,
|
| 362 |
+
pmi_uncond_prior_precompute=pmi_uncond_prior_precompute,
|
| 363 |
+
use_cuda_graph=use_cuda_graph,
|
| 364 |
+
backend=backend,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
wavs: list[torch.Tensor] = []
|
| 368 |
+
for i, sl in enumerate(speech_lens):
|
| 369 |
+
row = speech_tokens_batch[i : i + 1, :sl]
|
| 370 |
+
row = self._trim_to_eos(row[0]).unsqueeze(0)
|
| 371 |
+
wav, _ = self.s3gen.inference(
|
| 372 |
+
speech_tokens=row.to(self.device),
|
| 373 |
+
ref_dict=conds_list[i].gen,
|
| 374 |
+
n_cfm_timesteps=n_cfm_timesteps,
|
| 375 |
+
)
|
| 376 |
+
wavs.append(wav.detach().cpu().squeeze(0))
|
| 377 |
+
return wavs
|
| 378 |
+
|
| 379 |
+
# -------- internal ---------------------------------------------------
|
| 380 |
+
|
| 381 |
+
def _trim_to_eos(self, speech_tokens: torch.Tensor) -> torch.Tensor:
|
| 382 |
+
"""Truncate at the first stop-speech token and drop non-codec ids.
|
| 383 |
+
|
| 384 |
+
T3 generates ids in ``[0, hp.speech_tokens_dict_size)``, but S3Gen's
|
| 385 |
+
flow embedding is sized to the raw S3 codec vocab
|
| 386 |
+
(``hp.start_speech_token``). Anything ``>= start_speech_token`` —
|
| 387 |
+
the SOS / EOS specials, the ``[MASK]`` row, or never-unmasked slots —
|
| 388 |
+
must be removed before vocoding.
|
| 389 |
+
"""
|
| 390 |
+
if speech_tokens.numel() == 0:
|
| 391 |
+
return speech_tokens.long()
|
| 392 |
+
stop = self.t3.hp.stop_speech_token
|
| 393 |
+
eos = (speech_tokens == stop).nonzero(as_tuple=True)[0]
|
| 394 |
+
if len(eos) > 0:
|
| 395 |
+
speech_tokens = speech_tokens[: eos[0].item()]
|
| 396 |
+
codec_vocab = int(self.t3.hp.start_speech_token) # 6561 for chatterbox
|
| 397 |
+
speech_tokens = speech_tokens[speech_tokens < codec_vocab]
|
| 398 |
+
return speech_tokens.long()
|
runtime/chatterbox-flash/pyproject.toml
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "chatterbox-flash-nvfp4"
|
| 7 |
+
version = "0.1.0+nvfp4.1"
|
| 8 |
+
description = "Unofficial source-free NVFP4 W4A16 runtime for Chatterbox-Flash T3 on NVIDIA Blackwell."
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
license = { text = "MIT" }
|
| 11 |
+
requires-python = ">=3.10"
|
| 12 |
+
authors = [
|
| 13 |
+
{ name = "Resemble AI" },
|
| 14 |
+
{ name = "Community NVFP4 contributors" },
|
| 15 |
+
]
|
| 16 |
+
classifiers = [
|
| 17 |
+
"Development Status :: 4 - Beta",
|
| 18 |
+
"Intended Audience :: Developers",
|
| 19 |
+
"Intended Audience :: Science/Research",
|
| 20 |
+
"License :: OSI Approved :: MIT License",
|
| 21 |
+
"Programming Language :: Python :: 3",
|
| 22 |
+
"Programming Language :: Python :: 3.10",
|
| 23 |
+
"Programming Language :: Python :: 3.11",
|
| 24 |
+
"Programming Language :: Python :: 3.12",
|
| 25 |
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
| 26 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 27 |
+
]
|
| 28 |
+
dependencies = [
|
| 29 |
+
# T3 / S3Gen / VoiceEncoder / EnTokenizer come from chatterbox-tts. Note that
|
| 30 |
+
# chatterbox-tts 0.1.7 hard-pins ``torch==2.6.0``, ``torchaudio==2.6.0``,
|
| 31 |
+
# ``transformers==5.2.0``, ``librosa==0.11.0`` and ``safetensors==0.5.3``.
|
| 32 |
+
# We deliberately leave those ranges loose here and let the chatterbox-tts
|
| 33 |
+
# solve drive the install — otherwise uv flips them back-and-forth between
|
| 34 |
+
# each invocation.
|
| 35 |
+
"chatterbox-tts>=0.1.7",
|
| 36 |
+
"torch>=2.6",
|
| 37 |
+
"torchaudio>=2.6",
|
| 38 |
+
"transformers>=4.46",
|
| 39 |
+
"safetensors>=0.4",
|
| 40 |
+
"huggingface-hub>=0.24",
|
| 41 |
+
"numpy>=1.24",
|
| 42 |
+
"tqdm>=4.66",
|
| 43 |
+
"soundfile>=0.12",
|
| 44 |
+
"librosa>=0.10",
|
| 45 |
+
"scipy>=1.11",
|
| 46 |
+
"regex",
|
| 47 |
+
"inflect>=7.0",
|
| 48 |
+
"Unidecode>=1.3",
|
| 49 |
+
# Used by the top-level ``synthesize.py`` CLI to parse YAML configs in
|
| 50 |
+
# ``configs/`` (best / best_no_early_dec / fast). The library itself
|
| 51 |
+
# does not import PyYAML.
|
| 52 |
+
"pyyaml>=6.0",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
[project.optional-dependencies]
|
| 56 |
+
# FlashInfer kernels (required for batched / CUDA-graph inference).
|
| 57 |
+
# Install matching your CUDA build: pip install flashinfer-python
|
| 58 |
+
flashinfer = [
|
| 59 |
+
# The canonical loader depends on the CuTe-DSL W4A16 preparation and GEMM
|
| 60 |
+
# APIs validated for this release. Older 0.6.11 builds resolve from the
|
| 61 |
+
# upstream lock but do not reproduce the accepted runtime.
|
| 62 |
+
"flashinfer-python==0.6.14",
|
| 63 |
+
"nvidia-cutlass-dsl==4.5.2",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
# Apple Silicon (Mac) native Metal backend. Pulls in mlx + mlx-lm; the
|
| 67 |
+
# mlx-lm LLaMA implementation is used as the backbone with our custom
|
| 68 |
+
# inputs_embeds bypass. CPU/GPU paths do not import these.
|
| 69 |
+
mlx = [
|
| 70 |
+
"mlx>=0.13; sys_platform == 'darwin'",
|
| 71 |
+
"mlx-lm>=0.10; sys_platform == 'darwin'",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
# Evaluation suite (SIM-o / WER / UTMOS) via the OmniVoice pip package.
|
| 75 |
+
eval = [
|
| 76 |
+
"omnivoice>=0.1.5",
|
| 77 |
+
# omnivoice's WER eval imports the legacy ``jiwer.compute_measures``, which
|
| 78 |
+
# was removed in jiwer 4.0. Cap below 4 so the import keeps working.
|
| 79 |
+
"jiwer>=3.0,<4",
|
| 80 |
+
"zhon>=2.0",
|
| 81 |
+
"whisper-normalizer>=0.0.10",
|
| 82 |
+
# omnivoice's SIM-o (speaker similarity) loads the WavLM-large upstream via
|
| 83 |
+
# torch.hub, whose hubconf imports ``s3prl`` (s3prl.util.download +
|
| 84 |
+
# s3prl.upstream.wavlm.expert). omnivoice does not declare it, so pull it in
|
| 85 |
+
# here — otherwise Stage 2/3 SIM-o crashes with ModuleNotFoundError: s3prl.
|
| 86 |
+
"s3prl>=0.4",
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
[project.urls]
|
| 90 |
+
Upstream = "https://github.com/resemble-ai/chatterbox-flash"
|
| 91 |
+
BaseModel = "https://huggingface.co/ResembleAI/chatterbox-flash"
|
| 92 |
+
Model = "https://huggingface.co/ajh-code/chatterbox-flash-t3-w4a16-nvfp4"
|
| 93 |
+
|
| 94 |
+
[tool.setuptools.packages.find]
|
| 95 |
+
where = ["."]
|
| 96 |
+
include = ["chatterbox_flash*"]
|
| 97 |
+
|
| 98 |
+
[tool.setuptools.package-data]
|
| 99 |
+
chatterbox_flash = ["py.typed"]
|
| 100 |
+
|
| 101 |
+
# ----------------------------------------------------------------------------
|
| 102 |
+
# uv-specific configuration
|
| 103 |
+
# ----------------------------------------------------------------------------
|
| 104 |
+
#
|
| 105 |
+
# ``chatterbox-tts==0.1.7`` hard-pins ``torch==2.6.0`` / ``torchaudio==2.6.0`` /
|
| 106 |
+
# ``transformers==5.2.0``, but most modern wheels installed alongside it
|
| 107 |
+
# (``torchvision``, ``xformers``, ``flash-attn``, ``flashinfer-python``) ship
|
| 108 |
+
# only torch-2.7 ABI binaries. Letting uv honour the chatterbox-tts pin causes
|
| 109 |
+
# a cascade of ABI mismatches: ``torchvision::nms does not exist``,
|
| 110 |
+
# ``flash_attn_2_cuda.so: undefined symbol``, etc.
|
| 111 |
+
#
|
| 112 |
+
# We therefore override the upper-stream pins to ``>=2.7`` for the install
|
| 113 |
+
# itself. The chatterbox-tts code paths we use (T3 / S3Gen / VE / EnTokenizer)
|
| 114 |
+
# work correctly against torch 2.7 — only the install-time metadata is stale.
|
| 115 |
+
[tool.uv]
|
| 116 |
+
override-dependencies = [
|
| 117 |
+
# Pin to the 2.7.x ABI that the rest of the wheel ecosystem (torchvision,
|
| 118 |
+
# xformers, flash-attn, flashinfer-python) currently ships matched binaries
|
| 119 |
+
# against. Bumping the upper bound requires re-checking those wheels.
|
| 120 |
+
"torch==2.7.1",
|
| 121 |
+
"torchaudio==2.7.1",
|
| 122 |
+
"torchvision==0.22.1",
|
| 123 |
+
"transformers==5.9.0",
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
# Use the exact CUDA 12.8 wheels validated on Blackwell. Marking this index
|
| 127 |
+
# explicit keeps unrelated packages on PyPI while routing only the PyTorch
|
| 128 |
+
# family below to the official cu128 wheel repository.
|
| 129 |
+
[[tool.uv.index]]
|
| 130 |
+
name = "pytorch-cu128"
|
| 131 |
+
url = "https://download.pytorch.org/whl/cu128"
|
| 132 |
+
explicit = true
|
| 133 |
+
|
| 134 |
+
[tool.uv.sources]
|
| 135 |
+
torch = { index = "pytorch-cu128" }
|
| 136 |
+
torchaudio = { index = "pytorch-cu128" }
|
| 137 |
+
torchvision = { index = "pytorch-cu128" }
|
runtime/chatterbox-flash/uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
t3_nvfp4.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c5ea2d1d1441f657b383d8dad54bbae3778447d675822fb15918cb99b3587f1e
|
| 3 |
+
size 341439040
|