Spaces:
Running on Zero
Running on Zero
Commit ·
cbc0e90
1
Parent(s): 8010b00
Repair PersonaPlex for Blackwell ZeroGPU
Browse files- PERSONAPLEX_LICENSE-MIT +23 -0
- PERSONAPLEX_LICENSE.audiocraft +21 -0
- PERSONAPLEX_LICENSE.moshi +27 -0
- README.md +4 -0
- VENDORED_PERSONAPLEX.md +18 -0
- app.py +3 -0
- moshi/__init__.py +18 -0
- moshi/client_utils.py +213 -0
- moshi/models/__init__.py +14 -0
- moshi/models/compression.py +474 -0
- moshi/models/lm.py +1178 -0
- moshi/models/loaders.py +364 -0
- moshi/modules/__init__.py +23 -0
- moshi/modules/conv.py +350 -0
- moshi/modules/gating.py +103 -0
- moshi/modules/resample.py +140 -0
- moshi/modules/rope.py +111 -0
- moshi/modules/seanet.py +416 -0
- moshi/modules/streaming.py +630 -0
- moshi/modules/transformer.py +774 -0
- moshi/offline.py +431 -0
- moshi/quantization/__init__.py +13 -0
- moshi/quantization/base.py +170 -0
- moshi/quantization/core_vq.py +384 -0
- moshi/quantization/vq.py +361 -0
- moshi/server.py +483 -0
- moshi/utils/__init__.py +10 -0
- moshi/utils/autocast.py +45 -0
- moshi/utils/compile.py +308 -0
- moshi/utils/connection.py +228 -0
- moshi/utils/logging.py +75 -0
- moshi/utils/sampling.py +147 -0
- requirements.txt +11 -7
PERSONAPLEX_LICENSE-MIT
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Permission is hereby granted, free of charge, to any
|
| 2 |
+
person obtaining a copy of this software and associated
|
| 3 |
+
documentation files (the "Software"), to deal in the
|
| 4 |
+
Software without restriction, including without
|
| 5 |
+
limitation the rights to use, copy, modify, merge,
|
| 6 |
+
publish, distribute, sublicense, and/or sell copies of
|
| 7 |
+
the Software, and to permit persons to whom the Software
|
| 8 |
+
is furnished to do so, subject to the following
|
| 9 |
+
conditions:
|
| 10 |
+
|
| 11 |
+
The above copyright notice and this permission notice
|
| 12 |
+
shall be included in all copies or substantial portions
|
| 13 |
+
of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
| 16 |
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
| 17 |
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
| 18 |
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
| 19 |
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
| 20 |
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
| 21 |
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
| 22 |
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 23 |
+
DEALINGS IN THE SOFTWARE.
|
PERSONAPLEX_LICENSE.audiocraft
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 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.
|
PERSONAPLEX_LICENSE.moshi
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) Kyutai
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any
|
| 6 |
+
person obtaining a copy of this software and associated
|
| 7 |
+
documentation files (the "Software"), to deal in the
|
| 8 |
+
Software without restriction, including without
|
| 9 |
+
limitation the rights to use, copy, modify, merge,
|
| 10 |
+
publish, distribute, sublicense, and/or sell copies of
|
| 11 |
+
the Software, and to permit persons to whom the Software
|
| 12 |
+
is furnished to do so, subject to the following
|
| 13 |
+
conditions:
|
| 14 |
+
|
| 15 |
+
The above copyright notice and this permission notice
|
| 16 |
+
shall be included in all copies or substantial portions
|
| 17 |
+
of the Software.
|
| 18 |
+
|
| 19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
| 20 |
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
| 21 |
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
| 22 |
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
| 23 |
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
| 24 |
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
| 25 |
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
| 26 |
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 27 |
+
DEALINGS IN THE SOFTWARE.
|
README.md
CHANGED
|
@@ -61,6 +61,10 @@ ZeroGPU. PersonaPlex is the practical feasibility fallback.
|
|
| 61 |
- Model: `nvidia/personaplex-7b-v1`
|
| 62 |
- Model revision: `fdaf4090a61cb315c138a1faee287ffd6c716309`
|
| 63 |
- NVIDIA PersonaPlex source revision: `3428dfd95309a7f3c84fd93259ded0f810d1ff91`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
- Base ZeroGPU implementation reviewed from `Javedalam/PersonaPlex` at
|
| 65 |
`1b0acf6f195ec8d33e48907531909f1409c17d34`
|
| 66 |
|
|
|
|
| 61 |
- Model: `nvidia/personaplex-7b-v1`
|
| 62 |
- Model revision: `fdaf4090a61cb315c138a1faee287ffd6c716309`
|
| 63 |
- NVIDIA PersonaPlex source revision: `3428dfd95309a7f3c84fd93259ded0f810d1ff91`
|
| 64 |
+
- The required `moshi` Python package is vendored unchanged from that revision so RoleForge can use the
|
| 65 |
+
ZeroGPU-supported PyTorch 2.8 / CUDA 12.8 runtime. The upstream package metadata restricts PyTorch to `<2.5`,
|
| 66 |
+
which cannot execute on ZeroGPU's current Blackwell `sm_120` workers. The upstream MIT/Moshi/AudioCraft
|
| 67 |
+
license files are preserved at the repository root.
|
| 68 |
- Base ZeroGPU implementation reviewed from `Javedalam/PersonaPlex` at
|
| 69 |
`1b0acf6f195ec8d33e48907531909f1409c17d34`
|
| 70 |
|
VENDORED_PERSONAPLEX.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Vendored PersonaPlex runtime
|
| 2 |
+
|
| 3 |
+
RoleForge vendors the `moshi/moshi` Python source directory unchanged from NVIDIA PersonaPlex commit
|
| 4 |
+
`3428dfd95309a7f3c84fd93259ded0f810d1ff91`.
|
| 5 |
+
|
| 6 |
+
Upstream: <https://github.com/NVIDIA/personaplex>
|
| 7 |
+
|
| 8 |
+
Reason: the upstream package metadata requires `torch >=2.2,<2.5`. Hugging Face ZeroGPU now uses NVIDIA RTX Pro
|
| 9 |
+
6000 Blackwell workers (`sm_120`) and officially supports PyTorch 2.8 or newer. Installing the upstream package
|
| 10 |
+
normally therefore selected a PyTorch build with kernels only through `sm_90`, causing every real GPU turn to
|
| 11 |
+
fail with `no kernel image is available for execution on the device`.
|
| 12 |
+
|
| 13 |
+
The vendored Python files are unchanged. Only dependency resolution moved into RoleForge's `requirements.txt`,
|
| 14 |
+
which pins PyTorch 2.8.0 with CUDA 12.8. The corresponding upstream license files are preserved as:
|
| 15 |
+
|
| 16 |
+
- `PERSONAPLEX_LICENSE-MIT`
|
| 17 |
+
- `PERSONAPLEX_LICENSE.moshi`
|
| 18 |
+
- `PERSONAPLEX_LICENSE.audiocraft`
|
app.py
CHANGED
|
@@ -16,6 +16,9 @@ import torch
|
|
| 16 |
from huggingface_hub import hf_hub_download
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
MODEL_REPO = "nvidia/personaplex-7b-v1"
|
| 20 |
MODEL_REVISION = "fdaf4090a61cb315c138a1faee287ffd6c716309"
|
| 21 |
DEVICE = "cuda"
|
|
|
|
| 16 |
from huggingface_hub import hf_hub_download
|
| 17 |
|
| 18 |
|
| 19 |
+
print(f"RoleForge runtime: torch={torch.__version__}, cuda={torch.version.cuda}")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
MODEL_REPO = "nvidia/personaplex-7b-v1"
|
| 23 |
MODEL_REVISION = "fdaf4090a61cb315c138a1faee287ffd6c716309"
|
| 24 |
DEVICE = "cuda"
|
moshi/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
moshi is the inference codebase for Kyutai audio generation models.
|
| 7 |
+
|
| 8 |
+
The code has been adapted from Audiocraft, see LICENSE.audiocraft
|
| 9 |
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
# flake8: noqa
|
| 13 |
+
from . import utils
|
| 14 |
+
from . import modules
|
| 15 |
+
from . import models
|
| 16 |
+
from . import quantization
|
| 17 |
+
|
| 18 |
+
__version__ = "0.1.0"
|
moshi/client_utils.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
"""Utilities for the command line client, in particular for handling interactions with the terminal.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def colorize(text, color):
|
| 12 |
+
code = f"\033[{color}m"
|
| 13 |
+
restore = "\033[0m"
|
| 14 |
+
return "".join([code, text, restore])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def make_log(level: str, msg: str) -> str:
|
| 18 |
+
if level == "warning":
|
| 19 |
+
prefix = colorize("[Warn]", "1;31")
|
| 20 |
+
elif level == "info":
|
| 21 |
+
prefix = colorize("[Info]", "1;34")
|
| 22 |
+
elif level == "error":
|
| 23 |
+
prefix = colorize("[Err ]", "1;31")
|
| 24 |
+
else:
|
| 25 |
+
raise ValueError(f"Unknown level {level}")
|
| 26 |
+
return prefix + " " + msg
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class RawPrinter:
|
| 30 |
+
def __init__(self, stream=sys.stdout, err_stream=sys.stderr):
|
| 31 |
+
self.stream = stream
|
| 32 |
+
self.err_stream = err_stream
|
| 33 |
+
|
| 34 |
+
def print_header(self):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
def print_token(self, token: str):
|
| 38 |
+
self.stream.write(token)
|
| 39 |
+
self.stream.flush()
|
| 40 |
+
|
| 41 |
+
def log(self, level: str, msg: str):
|
| 42 |
+
print(f"{level.capitalize()}: {msg}", file=self.err_stream)
|
| 43 |
+
|
| 44 |
+
def print_lag(self):
|
| 45 |
+
self.err_stream.write(colorize(" [LAG]", "31"))
|
| 46 |
+
self.err_stream.flush()
|
| 47 |
+
|
| 48 |
+
def print_pending(self):
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@dataclass
|
| 53 |
+
class LineEntry:
|
| 54 |
+
msg: str
|
| 55 |
+
color: str | None = None
|
| 56 |
+
|
| 57 |
+
def render(self):
|
| 58 |
+
if self.color is None:
|
| 59 |
+
return self.msg
|
| 60 |
+
else:
|
| 61 |
+
return colorize(self.msg, self.color)
|
| 62 |
+
|
| 63 |
+
def __len__(self):
|
| 64 |
+
return len(self.msg)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class Line:
|
| 68 |
+
def __init__(self, stream):
|
| 69 |
+
self.stream = stream
|
| 70 |
+
self._line: list[LineEntry] = []
|
| 71 |
+
self._has_padding: bool = False
|
| 72 |
+
self._max_line_length = 0
|
| 73 |
+
|
| 74 |
+
def __bool__(self):
|
| 75 |
+
return bool(self._line)
|
| 76 |
+
|
| 77 |
+
def __len__(self):
|
| 78 |
+
return sum(len(entry) for entry in self._line)
|
| 79 |
+
|
| 80 |
+
def add(self, msg: str, color: str | None = None) -> int:
|
| 81 |
+
entry = LineEntry(msg, color)
|
| 82 |
+
return self._add(entry)
|
| 83 |
+
|
| 84 |
+
def _add(self, entry: LineEntry) -> int:
|
| 85 |
+
if self._has_padding:
|
| 86 |
+
self.erase(count=0)
|
| 87 |
+
self._line.append(entry)
|
| 88 |
+
self.stream.write(entry.render())
|
| 89 |
+
self._max_line_length = max(self._max_line_length, len(self))
|
| 90 |
+
return len(entry)
|
| 91 |
+
|
| 92 |
+
def erase(self, count: int = 1):
|
| 93 |
+
if count:
|
| 94 |
+
entries = list(self._line[:-count])
|
| 95 |
+
else:
|
| 96 |
+
entries = list(self._line)
|
| 97 |
+
self._line.clear()
|
| 98 |
+
self.stream.write("\r")
|
| 99 |
+
for entry in entries:
|
| 100 |
+
self._line.append(entry)
|
| 101 |
+
self.stream.write(entry.render())
|
| 102 |
+
|
| 103 |
+
self._has_padding = False
|
| 104 |
+
|
| 105 |
+
def newline(self):
|
| 106 |
+
missing = self._max_line_length - len(self)
|
| 107 |
+
if missing > 0:
|
| 108 |
+
self.stream.write(" " * missing)
|
| 109 |
+
self.stream.write("\n")
|
| 110 |
+
self._line.clear()
|
| 111 |
+
self._max_line_length = 0
|
| 112 |
+
self._has_padding = False
|
| 113 |
+
|
| 114 |
+
def flush(self):
|
| 115 |
+
missing = self._max_line_length - len(self)
|
| 116 |
+
if missing > 0:
|
| 117 |
+
self.stream.write(" " * missing)
|
| 118 |
+
self._has_padding = True
|
| 119 |
+
self.stream.flush()
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Printer:
|
| 123 |
+
def __init__(self, max_cols: int = 80, stream=sys.stdout, err_stream=sys.stderr):
|
| 124 |
+
self.max_cols = max_cols
|
| 125 |
+
self.line = Line(stream)
|
| 126 |
+
self.stream = stream
|
| 127 |
+
self.err_stream = err_stream
|
| 128 |
+
self._pending_count = 0
|
| 129 |
+
self._pending_printed = False
|
| 130 |
+
|
| 131 |
+
def print_header(self):
|
| 132 |
+
self.line.add(" " + "-" * (self.max_cols) + " ")
|
| 133 |
+
self.line.newline()
|
| 134 |
+
self.line.flush()
|
| 135 |
+
self.line.add("| ")
|
| 136 |
+
|
| 137 |
+
def _remove_pending(self) -> bool:
|
| 138 |
+
if self._pending_printed:
|
| 139 |
+
self._pending_printed = False
|
| 140 |
+
self.line.erase(1)
|
| 141 |
+
return True
|
| 142 |
+
return False
|
| 143 |
+
|
| 144 |
+
def print_token(self, token: str, color: str | None = None):
|
| 145 |
+
self._remove_pending()
|
| 146 |
+
remaining = self.max_cols - len(self.line)
|
| 147 |
+
if len(token) <= remaining:
|
| 148 |
+
self.line.add(token, color)
|
| 149 |
+
else:
|
| 150 |
+
end = " " * remaining + " |"
|
| 151 |
+
if token.startswith(" "):
|
| 152 |
+
token = token.lstrip()
|
| 153 |
+
self.line.add(end)
|
| 154 |
+
self.line.newline()
|
| 155 |
+
self.line.add("| ")
|
| 156 |
+
self.line.add(token, color)
|
| 157 |
+
else:
|
| 158 |
+
assert color is None
|
| 159 |
+
erase_count = None
|
| 160 |
+
cumulated = ""
|
| 161 |
+
for idx, entry in enumerate(self.line._line[::-1]):
|
| 162 |
+
if entry.color:
|
| 163 |
+
# probably a LAG message
|
| 164 |
+
erase_count = idx
|
| 165 |
+
break
|
| 166 |
+
if entry.msg.startswith(" "):
|
| 167 |
+
erase_count = idx + 1
|
| 168 |
+
cumulated = entry.msg + cumulated
|
| 169 |
+
break
|
| 170 |
+
if erase_count is not None:
|
| 171 |
+
if erase_count > 0:
|
| 172 |
+
self.line.erase(erase_count)
|
| 173 |
+
remaining = self.max_cols - len(self.line)
|
| 174 |
+
end = " " * remaining + " |"
|
| 175 |
+
self.line.add(end)
|
| 176 |
+
self.line.newline()
|
| 177 |
+
self.line.add("| ")
|
| 178 |
+
token = cumulated.lstrip() + token
|
| 179 |
+
self.line.add(token)
|
| 180 |
+
else:
|
| 181 |
+
self.line.add(token[:remaining])
|
| 182 |
+
self.line.add(" |")
|
| 183 |
+
self.line.newline()
|
| 184 |
+
self.line.add("| ")
|
| 185 |
+
self.line.add(token[remaining:])
|
| 186 |
+
self.line.flush()
|
| 187 |
+
|
| 188 |
+
def log(self, level: str, msg: str):
|
| 189 |
+
msg = make_log(level, msg)
|
| 190 |
+
self._remove_pending()
|
| 191 |
+
if self.line:
|
| 192 |
+
self.line.newline()
|
| 193 |
+
self.line.flush()
|
| 194 |
+
print(msg, file=self.err_stream)
|
| 195 |
+
self.err_stream.flush()
|
| 196 |
+
|
| 197 |
+
def print_lag(self):
|
| 198 |
+
self.print_token(" [LAG]", "31")
|
| 199 |
+
|
| 200 |
+
def print_pending(self):
|
| 201 |
+
chars = ["|", "/", "-", "\\"]
|
| 202 |
+
count = int(self._pending_count / 5)
|
| 203 |
+
char = chars[count % len(chars)]
|
| 204 |
+
colors = ["32", "33", "31"]
|
| 205 |
+
self._remove_pending()
|
| 206 |
+
self.line.add(char, colors[count % len(colors)])
|
| 207 |
+
self._pending_printed = True
|
| 208 |
+
self._pending_count += 1
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
AnyPrinter = Printer | RawPrinter
|
| 212 |
+
|
| 213 |
+
|
moshi/models/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
"""
|
| 5 |
+
Models for the compression model Moshi,
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# flake8: noqa
|
| 9 |
+
from .compression import (
|
| 10 |
+
CompressionModel,
|
| 11 |
+
MimiModel,
|
| 12 |
+
)
|
| 13 |
+
from .lm import LMModel, LMGen
|
| 14 |
+
from .loaders import get_mimi, get_moshi_lm
|
moshi/models/compression.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Part of this file is adapted from encodec.py in https://github.com/facebookresearch/audiocraft
|
| 6 |
+
# released under the following license.
|
| 7 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 8 |
+
# All rights reserved.
|
| 9 |
+
#
|
| 10 |
+
# This source code is licensed under the license found in the
|
| 11 |
+
# LICENSE file in the root directory of this source tree.
|
| 12 |
+
"""Compression models or wrapper around existing models. In particular, provides the implementation
|
| 13 |
+
for Mimi. Also defines the main interface that a model must follow to be usable as an audio tokenizer.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from abc import abstractmethod
|
| 17 |
+
from contextlib import nullcontext
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
import logging
|
| 20 |
+
import typing as tp
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
from torch import nn
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
from ..quantization import (
|
| 27 |
+
QuantizedResult,
|
| 28 |
+
BaseQuantizer,
|
| 29 |
+
SplitResidualVectorQuantizer,
|
| 30 |
+
ResidualVectorQuantizer,
|
| 31 |
+
)
|
| 32 |
+
from ..modules.resample import ConvDownsample1d, ConvTrUpsample1d
|
| 33 |
+
from ..modules.streaming import StreamingModule, State
|
| 34 |
+
from ..utils.compile import no_compile, CUDAGraphed
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
logger = logging.getLogger()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class CompressionModel(StreamingModule[State]):
|
| 41 |
+
"""Base API for all compression model that aim at being used as audio tokenizers
|
| 42 |
+
with a language model.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
@abstractmethod
|
| 46 |
+
def forward(self, x: torch.Tensor) -> QuantizedResult: ...
|
| 47 |
+
|
| 48 |
+
@abstractmethod
|
| 49 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 50 |
+
"""See `MimiModel.encode`."""
|
| 51 |
+
...
|
| 52 |
+
|
| 53 |
+
@abstractmethod
|
| 54 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
"""See `MimiModel.decode`."""
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
@abstractmethod
|
| 59 |
+
def decode_latent(self, codes: torch.Tensor) -> torch.Tensor:
|
| 60 |
+
"""Decode from the discrete codes to continuous latent space."""
|
| 61 |
+
...
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
@abstractmethod
|
| 65 |
+
def channels(self) -> int: ...
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
@abstractmethod
|
| 69 |
+
def frame_rate(self) -> float: ...
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
@abstractmethod
|
| 73 |
+
def sample_rate(self) -> int: ...
|
| 74 |
+
|
| 75 |
+
@property
|
| 76 |
+
@abstractmethod
|
| 77 |
+
def cardinality(self) -> int: ...
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
@abstractmethod
|
| 81 |
+
def num_codebooks(self) -> int: ...
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
@abstractmethod
|
| 85 |
+
def total_codebooks(self) -> int: ...
|
| 86 |
+
|
| 87 |
+
@abstractmethod
|
| 88 |
+
def set_num_codebooks(self, n: int):
|
| 89 |
+
"""Set the active number of codebooks used by the quantizer."""
|
| 90 |
+
...
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@dataclass
|
| 94 |
+
class _MimiState:
|
| 95 |
+
graphed_tr_enc: CUDAGraphed | None
|
| 96 |
+
graphed_tr_dec: CUDAGraphed | None
|
| 97 |
+
|
| 98 |
+
def reset(self):
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class MimiModel(CompressionModel[_MimiState]):
|
| 103 |
+
"""Mimi model operating on the raw waveform.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
encoder (nn.Module): Encoder network.
|
| 107 |
+
decoder (nn.Module): Decoder network.
|
| 108 |
+
quantizer (qt.BaseQuantizer): Quantizer network.
|
| 109 |
+
frame_rate (float): Final frame rate of the quantized representatiopn.
|
| 110 |
+
encoder_frame_rate (float): frame rate of the encoder model. Note that if `frame_rate != encopder_frame_rate`,
|
| 111 |
+
the latent will be resampled linearly to match the desired `frame_rate` before and after quantization.
|
| 112 |
+
sample_rate (int): Audio sample rate.
|
| 113 |
+
channels (int): Number of audio channels.
|
| 114 |
+
causal (bool): Whether to use a causal version of the model.
|
| 115 |
+
encoder_transformer (nn.Module or None): optional transformer for the encoder.
|
| 116 |
+
decoder_transformer (nn.Module or None): optional transformer for the decoder.
|
| 117 |
+
resample_method (str): method to use for resampling the latent space before the quantizer.
|
| 118 |
+
upsample_channel_wise_bug (bool): controls whether the upsampling is channel wise.
|
| 119 |
+
Defaults to true to reproduce bug in original implementation.
|
| 120 |
+
freeze_encoder: whether to freeze the encoder weights.
|
| 121 |
+
freeze_quantizer: whether to freeze the quantizer weights.
|
| 122 |
+
freeze_quantizer_level: If positive, freeze the quantizer up to this level.
|
| 123 |
+
torch_compile_encoder_decoder (bool): if True, uses torch.compile on the encoder / decoder.
|
| 124 |
+
Deactivated by default for training as this is incompatible at the moment with weight norm.
|
| 125 |
+
See https://github.com/pytorch/pytorch/issues/121902
|
| 126 |
+
Also this seems to work well with 2.2.0, but completely fail with 2.4.0.
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
def __init__(
|
| 130 |
+
self,
|
| 131 |
+
encoder: nn.Module,
|
| 132 |
+
decoder: nn.Module,
|
| 133 |
+
quantizer: BaseQuantizer,
|
| 134 |
+
frame_rate: float,
|
| 135 |
+
encoder_frame_rate: float,
|
| 136 |
+
sample_rate: int,
|
| 137 |
+
channels: int,
|
| 138 |
+
causal: bool = False,
|
| 139 |
+
encoder_transformer: tp.Optional[nn.Module] = None,
|
| 140 |
+
decoder_transformer: tp.Optional[nn.Module] = None,
|
| 141 |
+
resample_method: str = "interpolate",
|
| 142 |
+
upsample_channel_wise_bug: bool = True,
|
| 143 |
+
freeze_encoder: bool = False,
|
| 144 |
+
freeze_quantizer: bool = False,
|
| 145 |
+
freeze_quantizer_level: int = -1,
|
| 146 |
+
torch_compile_encoder_decoder: bool = False,
|
| 147 |
+
):
|
| 148 |
+
super().__init__()
|
| 149 |
+
self.encoder = encoder
|
| 150 |
+
self.decoder = decoder
|
| 151 |
+
self.encoder_transformer = encoder_transformer
|
| 152 |
+
self.decoder_transformer = decoder_transformer
|
| 153 |
+
self.quantizer = quantizer
|
| 154 |
+
self._frame_rate = frame_rate
|
| 155 |
+
self._sample_rate = sample_rate
|
| 156 |
+
self._channels = channels
|
| 157 |
+
self.encoder_frame_rate = encoder_frame_rate
|
| 158 |
+
self.torch_compile_encoder_decoder = torch_compile_encoder_decoder
|
| 159 |
+
|
| 160 |
+
if freeze_encoder:
|
| 161 |
+
for p in self.encoder.parameters():
|
| 162 |
+
p.requires_grad = False
|
| 163 |
+
if self.encoder_transformer is not None:
|
| 164 |
+
for p in self.encoder_transformer.parameters():
|
| 165 |
+
p.requires_grad = False
|
| 166 |
+
for name, p in self.quantizer.named_parameters():
|
| 167 |
+
if name.endswith("input_proj.weight"):
|
| 168 |
+
p.requires_grad = False
|
| 169 |
+
if freeze_quantizer:
|
| 170 |
+
self.quantizer.ema_frozen_(True)
|
| 171 |
+
self.freeze_quantizer = freeze_quantizer
|
| 172 |
+
self.freeze_quantizer_level = (
|
| 173 |
+
freeze_quantizer_level
|
| 174 |
+
if freeze_quantizer_level > 0
|
| 175 |
+
else self.quantizer.num_codebooks
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# We will need the dimension for the resampling. In general the encoder will be a SeanetEncoder
|
| 179 |
+
# which exposes a `dimension` attribute.
|
| 180 |
+
dimension = encoder.dimension
|
| 181 |
+
assert isinstance(
|
| 182 |
+
dimension, int
|
| 183 |
+
), f"Dimension should be int, got {dimension} of type {type(dimension)}."
|
| 184 |
+
self.dimension = dimension
|
| 185 |
+
|
| 186 |
+
assert resample_method in [
|
| 187 |
+
"interpolate",
|
| 188 |
+
"conv",
|
| 189 |
+
"avg_pool",
|
| 190 |
+
], f"Invalid resample_method {resample_method}"
|
| 191 |
+
self.resample_method = resample_method
|
| 192 |
+
if encoder_frame_rate != frame_rate:
|
| 193 |
+
assert not (
|
| 194 |
+
causal and resample_method == "interpolate"
|
| 195 |
+
), "Cannot interpolate with causal model."
|
| 196 |
+
if resample_method in ["conv", "avg_pool"]:
|
| 197 |
+
assert (
|
| 198 |
+
self.encoder_frame_rate > self.frame_rate
|
| 199 |
+
), "Cannot upsample with conv."
|
| 200 |
+
downsample_stride = self.encoder_frame_rate / self.frame_rate
|
| 201 |
+
assert downsample_stride == int(
|
| 202 |
+
downsample_stride
|
| 203 |
+
), f"Only integer strides are supported, got {downsample_stride}"
|
| 204 |
+
learnt = resample_method == "conv"
|
| 205 |
+
self.downsample = ConvDownsample1d(
|
| 206 |
+
int(downsample_stride),
|
| 207 |
+
dimension=dimension,
|
| 208 |
+
learnt=learnt,
|
| 209 |
+
causal=causal,
|
| 210 |
+
)
|
| 211 |
+
if freeze_encoder:
|
| 212 |
+
for p in self.downsample.parameters():
|
| 213 |
+
p.requires_grad = False
|
| 214 |
+
self.upsample = ConvTrUpsample1d(
|
| 215 |
+
int(downsample_stride),
|
| 216 |
+
dimension=dimension,
|
| 217 |
+
learnt=learnt,
|
| 218 |
+
causal=causal,
|
| 219 |
+
channel_wise=upsample_channel_wise_bug,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def _init_streaming_state(self, batch_size: int) -> _MimiState:
|
| 223 |
+
device = next(self.parameters()).device
|
| 224 |
+
disable = device.type != 'cuda'
|
| 225 |
+
graphed_tr_dec = None
|
| 226 |
+
graphed_tr_enc = None
|
| 227 |
+
if self.encoder_transformer is not None:
|
| 228 |
+
graphed_tr_enc = CUDAGraphed(self.encoder_transformer, disable=disable)
|
| 229 |
+
if self.decoder_transformer is not None:
|
| 230 |
+
graphed_tr_dec = CUDAGraphed(self.decoder_transformer, disable=disable)
|
| 231 |
+
return _MimiState(graphed_tr_enc, graphed_tr_dec)
|
| 232 |
+
|
| 233 |
+
@property
|
| 234 |
+
def channels(self) -> int:
|
| 235 |
+
return self._channels
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def frame_rate(self) -> float:
|
| 239 |
+
return self._frame_rate
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def sample_rate(self) -> int:
|
| 243 |
+
return self._sample_rate
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def total_codebooks(self):
|
| 247 |
+
"""Total number of quantizer codebooks available."""
|
| 248 |
+
return self.quantizer.total_codebooks
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def num_codebooks(self):
|
| 252 |
+
"""Active number of codebooks used by the quantizer."""
|
| 253 |
+
return self.quantizer.num_codebooks
|
| 254 |
+
|
| 255 |
+
def set_num_codebooks(self, n: int):
|
| 256 |
+
"""Set the active number of codebooks used by the quantizer."""
|
| 257 |
+
self.quantizer.set_num_codebooks(n)
|
| 258 |
+
|
| 259 |
+
@property
|
| 260 |
+
def cardinality(self):
|
| 261 |
+
"""Cardinality of each codebook."""
|
| 262 |
+
return self.quantizer.cardinality
|
| 263 |
+
|
| 264 |
+
def _to_framerate(self, x: torch.Tensor):
|
| 265 |
+
# Convert from the encoder frame rate to the overall framerate.
|
| 266 |
+
_, _, length = x.shape
|
| 267 |
+
frame_rate = self.encoder_frame_rate
|
| 268 |
+
new_frame_rate = self.frame_rate
|
| 269 |
+
if frame_rate == new_frame_rate:
|
| 270 |
+
return x
|
| 271 |
+
if self.resample_method == "interpolate":
|
| 272 |
+
target_length = int(length * new_frame_rate / frame_rate)
|
| 273 |
+
return nn.functional.interpolate(x, size=target_length, mode="linear")
|
| 274 |
+
else:
|
| 275 |
+
return self.downsample(x)
|
| 276 |
+
|
| 277 |
+
def _to_encoder_framerate(self, x: torch.Tensor):
|
| 278 |
+
# Convert from overall framerate to the encoder frame rate.
|
| 279 |
+
_, _, length = x.shape
|
| 280 |
+
frame_rate = self.encoder_frame_rate
|
| 281 |
+
new_frame_rate = self.frame_rate
|
| 282 |
+
if frame_rate == new_frame_rate:
|
| 283 |
+
return x
|
| 284 |
+
if self.resample_method == "interpolate":
|
| 285 |
+
target_length = int(length * new_frame_rate / frame_rate)
|
| 286 |
+
return nn.functional.interpolate(x, size=target_length, mode="linear")
|
| 287 |
+
else:
|
| 288 |
+
return self.upsample(x)
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def _context_for_encoder_decoder(self):
|
| 292 |
+
if self.torch_compile_encoder_decoder:
|
| 293 |
+
return nullcontext()
|
| 294 |
+
else:
|
| 295 |
+
return no_compile()
|
| 296 |
+
|
| 297 |
+
def forward(self, x: torch.Tensor) -> QuantizedResult:
|
| 298 |
+
assert x.dim() == 3
|
| 299 |
+
length = x.shape[-1]
|
| 300 |
+
extra_metrics: tp.Dict[str, torch.Tensor] = {}
|
| 301 |
+
|
| 302 |
+
if self.freeze_quantizer:
|
| 303 |
+
if isinstance(self.quantizer, SplitResidualVectorQuantizer):
|
| 304 |
+
self.quantizer.rvq_first.eval()
|
| 305 |
+
for i in range(
|
| 306 |
+
self.freeze_quantizer_level - self.quantizer.n_q_semantic
|
| 307 |
+
):
|
| 308 |
+
self.quantizer.rvq_rest.vq.layers[i].eval()
|
| 309 |
+
elif isinstance(self.quantizer, ResidualVectorQuantizer):
|
| 310 |
+
for i in range(self.freeze_quantizer_level):
|
| 311 |
+
self.quantizer.vq.layers[i].eval()
|
| 312 |
+
else:
|
| 313 |
+
raise ValueError(f"Unsupported quantizer type {type(self.quantizer)}")
|
| 314 |
+
|
| 315 |
+
with self._context_for_encoder_decoder:
|
| 316 |
+
emb = self.encoder(x)
|
| 317 |
+
if self.encoder_transformer is not None:
|
| 318 |
+
(emb,) = self.encoder_transformer(emb)
|
| 319 |
+
emb = self._to_framerate(emb)
|
| 320 |
+
expected_length = self.frame_rate * length / self.sample_rate
|
| 321 |
+
# Checking that we have the proper length given the advertised frame rate.
|
| 322 |
+
assert abs(emb.shape[-1] - expected_length) < 1, (
|
| 323 |
+
emb.shape[-1],
|
| 324 |
+
expected_length,
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
q_res = self.quantizer(emb, self.frame_rate)
|
| 328 |
+
emb = q_res.x
|
| 329 |
+
emb = self._to_encoder_framerate(emb)
|
| 330 |
+
if self.decoder_transformer is not None:
|
| 331 |
+
(emb,) = self.decoder_transformer(emb)
|
| 332 |
+
|
| 333 |
+
with self._context_for_encoder_decoder:
|
| 334 |
+
out = self.decoder(emb)
|
| 335 |
+
|
| 336 |
+
# remove extra padding added by the encoder and decoder
|
| 337 |
+
assert out.shape[-1] >= length, (out.shape[-1], length)
|
| 338 |
+
out = out[..., :length]
|
| 339 |
+
|
| 340 |
+
q_res.x = out
|
| 341 |
+
q_res.metrics.update(extra_metrics)
|
| 342 |
+
return q_res
|
| 343 |
+
|
| 344 |
+
def _encode_to_unquantized_latent(self, x: torch.Tensor) -> torch.Tensor:
|
| 345 |
+
"""Projects a batch of waveforms to unquantized latent space.
|
| 346 |
+
|
| 347 |
+
Args:
|
| 348 |
+
x (torch.Tensor): Float tensor of shape [B, C, T].
|
| 349 |
+
|
| 350 |
+
Returns:
|
| 351 |
+
Unquantized embeddings.
|
| 352 |
+
"""
|
| 353 |
+
assert (
|
| 354 |
+
x.dim() == 3
|
| 355 |
+
), f"CompressionModel._encode_to_unquantized_latent expects audio of shape [B, C, T] but got {x.shape}"
|
| 356 |
+
state = self._streaming_state
|
| 357 |
+
with self._context_for_encoder_decoder:
|
| 358 |
+
emb = self.encoder(x)
|
| 359 |
+
if self.encoder_transformer is not None:
|
| 360 |
+
if state is None:
|
| 361 |
+
(emb,) = self.encoder_transformer(emb)
|
| 362 |
+
else:
|
| 363 |
+
assert state.graphed_tr_enc is not None
|
| 364 |
+
(emb,) = state.graphed_tr_enc(emb)
|
| 365 |
+
emb = self._to_framerate(emb)
|
| 366 |
+
return emb
|
| 367 |
+
|
| 368 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 369 |
+
"""Encode the given input tensor to quantized representation.
|
| 370 |
+
|
| 371 |
+
Args:
|
| 372 |
+
x (torch.Tensor): Float tensor of shape [B, C, T]
|
| 373 |
+
|
| 374 |
+
Returns:
|
| 375 |
+
codes (torch.Tensor): an int tensor of shape [B, K, T]
|
| 376 |
+
with K the number of codebooks used and T the timestep.
|
| 377 |
+
"""
|
| 378 |
+
emb = self._encode_to_unquantized_latent(x)
|
| 379 |
+
codes = self.quantizer.encode(emb)
|
| 380 |
+
return codes
|
| 381 |
+
|
| 382 |
+
def encode_to_latent(self, x: torch.Tensor, quantize: bool = True) -> torch.Tensor:
|
| 383 |
+
"""Projects a batch of waveforms to latent space.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
x (torch.Tensor): Float tensor of shape [B, C, T].
|
| 387 |
+
|
| 388 |
+
Returns:
|
| 389 |
+
Embeddings, either quantized or not.
|
| 390 |
+
"""
|
| 391 |
+
emb = self._encode_to_unquantized_latent(x)
|
| 392 |
+
if not quantize:
|
| 393 |
+
return emb
|
| 394 |
+
else:
|
| 395 |
+
codes = self.quantizer.encode(emb)
|
| 396 |
+
return self.decode_latent(codes)
|
| 397 |
+
|
| 398 |
+
def decode(self, codes: torch.Tensor):
|
| 399 |
+
"""Decode the given codes to a reconstructed representation.
|
| 400 |
+
|
| 401 |
+
Args:
|
| 402 |
+
codes (torch.Tensor): Int tensor of shape [B, K, T]
|
| 403 |
+
|
| 404 |
+
Returns:
|
| 405 |
+
out (torch.Tensor): Float tensor of shape [B, C, T], the reconstructed audio.
|
| 406 |
+
"""
|
| 407 |
+
state = self._streaming_state
|
| 408 |
+
emb = self.decode_latent(codes)
|
| 409 |
+
emb = self._to_encoder_framerate(emb)
|
| 410 |
+
if self.decoder_transformer is not None:
|
| 411 |
+
if state is None:
|
| 412 |
+
(emb,) = self.decoder_transformer(emb)
|
| 413 |
+
else:
|
| 414 |
+
assert state.graphed_tr_dec is not None
|
| 415 |
+
(emb,) = state.graphed_tr_dec(emb)
|
| 416 |
+
with self._context_for_encoder_decoder:
|
| 417 |
+
out = self.decoder(emb)
|
| 418 |
+
# out contains extra padding added by the encoder and decoder
|
| 419 |
+
return out
|
| 420 |
+
|
| 421 |
+
def decode_latent(self, codes: torch.Tensor) -> torch.Tensor:
|
| 422 |
+
"""Decode from the discrete codes to continuous latent space."""
|
| 423 |
+
return self.quantizer.decode(codes)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class WrapperCompressionModel(CompressionModel[State]):
|
| 427 |
+
"""Base API for CompressionModel wrappers that do not depend on external frameworks."""
|
| 428 |
+
|
| 429 |
+
def __init__(self, model: CompressionModel):
|
| 430 |
+
super().__init__()
|
| 431 |
+
self.model = model
|
| 432 |
+
|
| 433 |
+
def forward(self, x: torch.Tensor) -> QuantizedResult:
|
| 434 |
+
return self.model.forward(x)
|
| 435 |
+
|
| 436 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 437 |
+
return self.model.encode(x)
|
| 438 |
+
|
| 439 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 440 |
+
return self.model.decode(codes)
|
| 441 |
+
|
| 442 |
+
def decode_latent(self, codes: torch.Tensor) -> torch.Tensor:
|
| 443 |
+
return self.model.decode_latent(codes)
|
| 444 |
+
|
| 445 |
+
def set_num_codebooks(self, n: int):
|
| 446 |
+
self.model.set_num_codebooks(n)
|
| 447 |
+
|
| 448 |
+
@property
|
| 449 |
+
def quantizer(self):
|
| 450 |
+
return self.model.quantizer
|
| 451 |
+
|
| 452 |
+
@property
|
| 453 |
+
def channels(self) -> int:
|
| 454 |
+
return self.model.channels
|
| 455 |
+
|
| 456 |
+
@property
|
| 457 |
+
def frame_rate(self) -> float:
|
| 458 |
+
return self.model.frame_rate
|
| 459 |
+
|
| 460 |
+
@property
|
| 461 |
+
def sample_rate(self) -> int:
|
| 462 |
+
return self.model.sample_rate
|
| 463 |
+
|
| 464 |
+
@property
|
| 465 |
+
def cardinality(self) -> int:
|
| 466 |
+
return self.model.cardinality
|
| 467 |
+
|
| 468 |
+
@property
|
| 469 |
+
def num_codebooks(self) -> int:
|
| 470 |
+
return self.model.num_codebooks
|
| 471 |
+
|
| 472 |
+
@property
|
| 473 |
+
def total_codebooks(self) -> int:
|
| 474 |
+
return self.model.total_codebooks
|
moshi/models/lm.py
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
from collections.abc import Callable
|
| 33 |
+
from dataclasses import dataclass
|
| 34 |
+
from functools import partial
|
| 35 |
+
from os.path import splitext
|
| 36 |
+
import logging
|
| 37 |
+
import numpy as np
|
| 38 |
+
import sys
|
| 39 |
+
from typing import Optional, Union, List, Tuple, Callable, Iterator
|
| 40 |
+
import sphn
|
| 41 |
+
import torch
|
| 42 |
+
from tqdm.auto import tqdm
|
| 43 |
+
|
| 44 |
+
from ..utils.sampling import sample_token
|
| 45 |
+
from ..utils.compile import CUDAGraphed
|
| 46 |
+
from ..modules.streaming import StreamingStateDict, StreamingContainer, StreamingModule, load_streaming_state
|
| 47 |
+
from ..modules.transformer import (
|
| 48 |
+
StreamingTransformer,
|
| 49 |
+
create_norm_fn,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
logger = logging.getLogger(__name__)
|
| 53 |
+
|
| 54 |
+
AUDIO_TOKENS_PER_STREAM = 8
|
| 55 |
+
FRAME_RATE_HZ = 12.5
|
| 56 |
+
SILENCE_TOKENS = np.array([948, 243, 1178, 546, 1736, 1030, 1978, 2008], dtype=np.int64)
|
| 57 |
+
SINE_TOKENS = np.array([430, 1268, 381, 1611, 1095, 1495, 56, 472], dtype=np.int64)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass
|
| 61 |
+
class LMOutput:
|
| 62 |
+
# The logits are already re-aligned with the input codes
|
| 63 |
+
# hence no extra shift is required, e.g. when computing CE
|
| 64 |
+
logits: torch.Tensor # [B, K, T, card]
|
| 65 |
+
mask: torch.Tensor # [B, K, T]
|
| 66 |
+
text_logits: torch.Tensor # [B, 1, T, text_card]
|
| 67 |
+
text_mask: torch.Tensor # [B, 1, T]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _delay_sequence(delays: List[int], tensor: torch.Tensor, padding: torch.Tensor) -> torch.Tensor:
|
| 71 |
+
B, K, T = tensor.shape
|
| 72 |
+
assert len(delays) == K, (len(delays), K)
|
| 73 |
+
outs = []
|
| 74 |
+
|
| 75 |
+
for k, delay in enumerate(delays):
|
| 76 |
+
assert delay >= 0
|
| 77 |
+
line = tensor[:, k].roll(delay, dims=1)
|
| 78 |
+
if delay > 0:
|
| 79 |
+
line[:, :delay] = padding[:, k]
|
| 80 |
+
outs.append(line)
|
| 81 |
+
return torch.stack(outs, dim=1)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _undelay_sequence(delays: List[int], tensor: torch.Tensor,
|
| 85 |
+
fill_value: Union[int, float] = float('NaN')) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 86 |
+
B, K, T, *_ = tensor.shape
|
| 87 |
+
assert len(delays) == K
|
| 88 |
+
mask = torch.ones(B, K, T, dtype=torch.bool, device=tensor.device)
|
| 89 |
+
outs = []
|
| 90 |
+
if all([delay == 0 for delay in delays]):
|
| 91 |
+
return tensor, mask
|
| 92 |
+
for k, delay in enumerate(delays):
|
| 93 |
+
assert delay >= 0
|
| 94 |
+
line = tensor[:, k].roll(-delay, dims=1)
|
| 95 |
+
if delay > 0:
|
| 96 |
+
line[:, -delay:] = fill_value
|
| 97 |
+
mask[:, k, -delay:] = 0
|
| 98 |
+
outs.append(line)
|
| 99 |
+
return torch.stack(outs, dim=1), mask
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def create_sinewave(duration: float, sample_rate: int) -> np.ndarray:
|
| 103 |
+
"""Return a 440 Hz 'silent' sinewave of the given duration."""
|
| 104 |
+
t = np.linspace(0.0, duration, int(sample_rate * duration), endpoint=False)
|
| 105 |
+
amplitude = 0.5
|
| 106 |
+
return amplitude * np.sin(2 * np.pi * 440.0 * t).astype(np.float32)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def normalize_audio(wav: np.ndarray, sr: int, target_lufs: float) -> np.ndarray:
|
| 110 |
+
"""Normalize **mono** audio to a target LUFS level."""
|
| 111 |
+
import pyloudnorm as pyln
|
| 112 |
+
# Ensure shape is (T,)
|
| 113 |
+
if wav.ndim == 2 and wav.shape[0] == 1:
|
| 114 |
+
wav = wav[0]
|
| 115 |
+
|
| 116 |
+
meter = pyln.Meter(sr)
|
| 117 |
+
loudness = meter.integrated_loudness(wav)
|
| 118 |
+
return pyln.normalize.loudness(wav, loudness, target_lufs)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def load_audio(
|
| 122 |
+
filepath: str, sample_rate: int,
|
| 123 |
+
):
|
| 124 |
+
"""Yields audio samples in intervals of sample_interval_size"""
|
| 125 |
+
sample_pcm, sample_sr = sphn.read(filepath)
|
| 126 |
+
sample_pcm = sphn.resample(
|
| 127 |
+
sample_pcm, src_sample_rate=sample_sr, dst_sample_rate=sample_rate
|
| 128 |
+
) # shape: (C, T)
|
| 129 |
+
return sample_pcm
|
| 130 |
+
|
| 131 |
+
def _iterate_audio(sample_pcm, sample_interval_size, max_len=sys.maxsize, pad=True):
|
| 132 |
+
cnt = 0
|
| 133 |
+
while sample_pcm.shape[-1] > 0 and cnt < max_len:
|
| 134 |
+
sample = sample_pcm[:, :sample_interval_size]
|
| 135 |
+
sample_pcm = sample_pcm[:, sample_interval_size:]
|
| 136 |
+
if sample_pcm.shape[-1] == 0 and pad:
|
| 137 |
+
sample = np.concatenate(
|
| 138 |
+
[
|
| 139 |
+
sample,
|
| 140 |
+
np.zeros(
|
| 141 |
+
(
|
| 142 |
+
sample.shape[0],
|
| 143 |
+
sample_interval_size - sample.shape[-1],
|
| 144 |
+
)
|
| 145 |
+
),
|
| 146 |
+
],
|
| 147 |
+
axis=1,
|
| 148 |
+
)
|
| 149 |
+
cnt += 1
|
| 150 |
+
yield sample[0:1] # shape: (1, T)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def encode_from_sphn(mimi, samples, max_batch=sys.maxsize):
|
| 154 |
+
"""
|
| 155 |
+
Takes an iterator of samples, batches them, encodes them;
|
| 156 |
+
and yields the encoded samples one sample at a time in the same order.
|
| 157 |
+
"""
|
| 158 |
+
device = next(mimi.parameters()).device
|
| 159 |
+
current_batch = []
|
| 160 |
+
done_flag = False
|
| 161 |
+
# TO-DO: Fix the batching bug
|
| 162 |
+
max_batch = 1
|
| 163 |
+
|
| 164 |
+
while True:
|
| 165 |
+
try:
|
| 166 |
+
sample = next(samples)
|
| 167 |
+
tensor = torch.tensor(sample, dtype=torch.float32, device=device)
|
| 168 |
+
tensor = tensor.unsqueeze(0) # shape: (1, C, T)
|
| 169 |
+
current_batch.append(tensor)
|
| 170 |
+
except StopIteration:
|
| 171 |
+
done_flag = True
|
| 172 |
+
|
| 173 |
+
if (not done_flag) and len(current_batch) < max_batch:
|
| 174 |
+
continue
|
| 175 |
+
if not current_batch:
|
| 176 |
+
break
|
| 177 |
+
|
| 178 |
+
batch = torch.cat(current_batch, dim=0) # shape: (B, C, T)
|
| 179 |
+
encoded = mimi.encode(batch) # shape: (B, K, F)
|
| 180 |
+
separated = torch.unbind(encoded, dim=0) # shape: (K, F)
|
| 181 |
+
reshaped = [x.unsqueeze(0) for x in separated] # shape: (1, K, F)
|
| 182 |
+
detached = [x.detach().clone() for x in reshaped]
|
| 183 |
+
|
| 184 |
+
current_batch = []
|
| 185 |
+
yield from detached # shape: (1, K, F)
|
| 186 |
+
|
| 187 |
+
if done_flag:
|
| 188 |
+
break
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class ScaledEmbedding(torch.nn.Embedding):
|
| 192 |
+
"""Boost learning rate for embeddings (with `scale`).
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
norm (bool): if True, uses a layer norm after the embedding.
|
| 196 |
+
zero_idx (int): special value indicating that the output should be exactly 0.
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
def __init__(self, *args, norm: bool = False, zero_idx: int = -1, **kwargs):
|
| 200 |
+
super().__init__(*args, **kwargs)
|
| 201 |
+
self.norm = None
|
| 202 |
+
if norm:
|
| 203 |
+
self.norm = create_norm_fn("layer_norm", self.embedding_dim)
|
| 204 |
+
assert zero_idx < 0, "Please use negative values for the zero_idx."
|
| 205 |
+
self.zero_idx = zero_idx
|
| 206 |
+
|
| 207 |
+
def forward(self, input, *args, **kwargs):
|
| 208 |
+
is_zero = input == self.zero_idx
|
| 209 |
+
zero = torch.zeros(1, dtype=input.dtype, device=input.device)
|
| 210 |
+
input = input.clamp(min=0)
|
| 211 |
+
y = super().forward(input, *args, **kwargs)
|
| 212 |
+
if self.norm is not None:
|
| 213 |
+
y = self.norm(y)
|
| 214 |
+
y = torch.where(is_zero[..., None], zero, y)
|
| 215 |
+
return y
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class LMModel(StreamingContainer):
|
| 219 |
+
"""Transformer-based language model on multiple streams of codes.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
n_q (int): Number of parallel streams to model as input.
|
| 223 |
+
dep_q (int): Number of parallel streams to model in the depformer.
|
| 224 |
+
card (int): Cardinality, vocabulary size.
|
| 225 |
+
text_card (int): Cardinality of the text vocabulary.
|
| 226 |
+
dim (int): Dimension of the transformer encoder.
|
| 227 |
+
num_heads (int): Number of heads for the transformer encoder.
|
| 228 |
+
hidden_scale (int): Scale for hidden feed forward dimension of the transformer encoder.
|
| 229 |
+
norm (str): Normalization method.
|
| 230 |
+
norm_emb (bool): Whether to normalize embeddings.
|
| 231 |
+
bias_proj (bool): Use bias for output projections.
|
| 232 |
+
depformer_*: params used for the Depformer Transformer, all the other will be shared.
|
| 233 |
+
depformer_multi_linear (bool): if True, uses one linear layer per codebook to project the
|
| 234 |
+
output of the main transformer to the Depformer latent space.
|
| 235 |
+
depformer_dim_feedforward (int| list[int]| None): If None, defaults to hidden_scale * depformer_dim.
|
| 236 |
+
existing_text_padding_id (bool): if True, will use a different token for the initial text token, and
|
| 237 |
+
the text padding token.
|
| 238 |
+
same_initial (bool): if True, uses the same initial tokens for both text and audio mode.
|
| 239 |
+
**kwargs: Additional parameters for the transformer encoder.
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
def __init__(
|
| 243 |
+
self,
|
| 244 |
+
delays: List[int] = [0],
|
| 245 |
+
n_q: int = 8,
|
| 246 |
+
dep_q: int = 8,
|
| 247 |
+
card: int = 1024,
|
| 248 |
+
text_card: int = 32000,
|
| 249 |
+
dim: int = 128,
|
| 250 |
+
num_heads: int = 8,
|
| 251 |
+
hidden_scale: int = 4,
|
| 252 |
+
norm: str = "layer_norm",
|
| 253 |
+
norm_emb: bool = False,
|
| 254 |
+
bias_proj: bool = False,
|
| 255 |
+
depformer_dim: int = 256,
|
| 256 |
+
depformer_dim_feedforward: int | list[int] | None = None,
|
| 257 |
+
depformer_multi_linear: bool = False,
|
| 258 |
+
depformer_weights_per_step: bool = False,
|
| 259 |
+
depformer_weights_per_step_schedule: list[int] | None = None,
|
| 260 |
+
depformer_pos_emb: str = "sin",
|
| 261 |
+
existing_text_padding_id: Optional[int] = None,
|
| 262 |
+
context: Optional[int] = None,
|
| 263 |
+
device=None,
|
| 264 |
+
dtype=None,
|
| 265 |
+
**kwargs,
|
| 266 |
+
):
|
| 267 |
+
super().__init__()
|
| 268 |
+
self.n_q = n_q
|
| 269 |
+
self.dep_q = dep_q
|
| 270 |
+
self.card = card
|
| 271 |
+
self.text_card = text_card
|
| 272 |
+
assert len(delays) == self.num_codebooks, "unexpected number of delays"
|
| 273 |
+
self.delays = delays
|
| 274 |
+
self.dim = dim
|
| 275 |
+
self.existing_text_padding_id = existing_text_padding_id
|
| 276 |
+
self.context = context
|
| 277 |
+
self.depformer_weights_per_step_schedule = depformer_weights_per_step_schedule
|
| 278 |
+
if depformer_weights_per_step_schedule is not None:
|
| 279 |
+
assert len(depformer_weights_per_step_schedule) == dep_q
|
| 280 |
+
kwargs["context"] = context
|
| 281 |
+
EmbeddingFactory = partial(
|
| 282 |
+
ScaledEmbedding,
|
| 283 |
+
norm=norm_emb,
|
| 284 |
+
device=device,
|
| 285 |
+
dtype=dtype,
|
| 286 |
+
zero_idx=self.zero_token_id,
|
| 287 |
+
)
|
| 288 |
+
self.EmbeddingFactory = EmbeddingFactory
|
| 289 |
+
self.emb = torch.nn.ModuleList(
|
| 290 |
+
[EmbeddingFactory(self.card + 1, dim) for _ in range(n_q)]
|
| 291 |
+
)
|
| 292 |
+
# Text card + padding token (if not in the original tokenizer)
|
| 293 |
+
extra_text = self.existing_text_padding_id is None
|
| 294 |
+
# Unlike for audio, here we authorize the model to output the special token.
|
| 295 |
+
self.text_emb = EmbeddingFactory(text_card + 1, dim)
|
| 296 |
+
self.text_linear = torch.nn.Linear(dim, text_card + extra_text, bias=bias_proj)
|
| 297 |
+
depformer_prefix = "depformer_"
|
| 298 |
+
main_kwargs = {
|
| 299 |
+
k: v for k, v in kwargs.items() if not k.startswith(depformer_prefix)
|
| 300 |
+
}
|
| 301 |
+
self.transformer = StreamingTransformer(
|
| 302 |
+
d_model=dim,
|
| 303 |
+
num_heads=num_heads,
|
| 304 |
+
dim_feedforward=int(hidden_scale * dim),
|
| 305 |
+
norm=norm,
|
| 306 |
+
device=device,
|
| 307 |
+
dtype=dtype,
|
| 308 |
+
**main_kwargs,
|
| 309 |
+
)
|
| 310 |
+
self.out_norm = create_norm_fn(norm, dim)
|
| 311 |
+
self.depformer_multi_linear = depformer_multi_linear
|
| 312 |
+
kwargs_dep = main_kwargs.copy()
|
| 313 |
+
kwargs_dep.update(
|
| 314 |
+
{
|
| 315 |
+
k.removeprefix(depformer_prefix): v
|
| 316 |
+
for k, v in kwargs.items()
|
| 317 |
+
if k.startswith(depformer_prefix)
|
| 318 |
+
}
|
| 319 |
+
)
|
| 320 |
+
kwargs_dep["positional_embedding"] = depformer_pos_emb
|
| 321 |
+
kwargs_dep["context"] = None
|
| 322 |
+
if depformer_weights_per_step:
|
| 323 |
+
kwargs_dep["weights_per_step"] = dep_q
|
| 324 |
+
if depformer_multi_linear:
|
| 325 |
+
# One linear layer per codebook to project different informations from the main model.
|
| 326 |
+
self.depformer_in = torch.nn.ModuleList(
|
| 327 |
+
[torch.nn.Linear(dim, depformer_dim, bias=False) for _ in range(dep_q)]
|
| 328 |
+
)
|
| 329 |
+
else:
|
| 330 |
+
self.depformer_in = torch.nn.ModuleList(
|
| 331 |
+
[torch.nn.Linear(dim, depformer_dim, bias=False)]
|
| 332 |
+
)
|
| 333 |
+
# Only using up to dep_q - 1 because the last codebook is never an input to Depformer.
|
| 334 |
+
self.depformer_emb = torch.nn.ModuleList(
|
| 335 |
+
[EmbeddingFactory(self.card + 1, depformer_dim) for _ in range(dep_q - 1)]
|
| 336 |
+
)
|
| 337 |
+
self.depformer_text_emb = EmbeddingFactory(text_card + 1, depformer_dim)
|
| 338 |
+
if depformer_dim_feedforward is None:
|
| 339 |
+
depformer_dim_feedforward = int(hidden_scale * depformer_dim)
|
| 340 |
+
self.depformer = StreamingTransformer(
|
| 341 |
+
d_model=depformer_dim,
|
| 342 |
+
dim_feedforward=depformer_dim_feedforward,
|
| 343 |
+
norm=norm,
|
| 344 |
+
device=device,
|
| 345 |
+
dtype=dtype,
|
| 346 |
+
**kwargs_dep,
|
| 347 |
+
)
|
| 348 |
+
self.depformer.set_streaming_propagate(False)
|
| 349 |
+
dim = depformer_dim # we will directly apply the next linears to the output of the Depformer.
|
| 350 |
+
|
| 351 |
+
self.linears = torch.nn.ModuleList(
|
| 352 |
+
[torch.nn.Linear(dim, self.card, bias=bias_proj) for _ in range(dep_q)]
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
@property
|
| 356 |
+
def initial_token_id(self) -> int:
|
| 357 |
+
"""Token id for the start of sequence (audio)."""
|
| 358 |
+
return self.card
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def text_initial_token_id(self) -> int:
|
| 362 |
+
"""Token id for the start of sequence (text)."""
|
| 363 |
+
return self.text_card
|
| 364 |
+
|
| 365 |
+
@property
|
| 366 |
+
def text_padding_token_id(self) -> int:
|
| 367 |
+
"""Token id for text padding."""
|
| 368 |
+
if self.existing_text_padding_id is None:
|
| 369 |
+
return self.text_card
|
| 370 |
+
else:
|
| 371 |
+
return self.existing_text_padding_id
|
| 372 |
+
|
| 373 |
+
@property
|
| 374 |
+
def end_of_text_padding_id(self) -> int:
|
| 375 |
+
"""Token id for optionally marking the last padding step for a word."""
|
| 376 |
+
return 0
|
| 377 |
+
|
| 378 |
+
@property
|
| 379 |
+
def zero_token_id(self) -> int:
|
| 380 |
+
"""Special value in the input tokens, indicating that no sampling should
|
| 381 |
+
happen for that value, and no input should be given to the model."""
|
| 382 |
+
return -1
|
| 383 |
+
|
| 384 |
+
@property
|
| 385 |
+
def ungenerated_token_id(self) -> int:
|
| 386 |
+
"""Special value that can be provided in the prompt to indicate that this specific
|
| 387 |
+
value should be predicted and sampled. This allows for partial teacher forcing, by generating
|
| 388 |
+
one modality, with the other one fixed.
|
| 389 |
+
"""
|
| 390 |
+
return -2
|
| 391 |
+
|
| 392 |
+
@property
|
| 393 |
+
def device(self):
|
| 394 |
+
first_param = next(iter(self.parameters()))
|
| 395 |
+
return first_param.device
|
| 396 |
+
|
| 397 |
+
@property
|
| 398 |
+
def num_codebooks(self) -> int:
|
| 399 |
+
return self.n_q + 1
|
| 400 |
+
|
| 401 |
+
@property
|
| 402 |
+
def num_audio_codebooks(self) -> int:
|
| 403 |
+
return self.n_q
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def audio_offset(self) -> int:
|
| 407 |
+
return 1
|
| 408 |
+
|
| 409 |
+
def _get_initial_token(self) -> torch.Tensor:
|
| 410 |
+
# Returns the initial token that will be fed to the model to predict the very first timestep.
|
| 411 |
+
# The output shape will be [B, K, 1].
|
| 412 |
+
device = next(iter(self.parameters())).device
|
| 413 |
+
zero = torch.full(
|
| 414 |
+
[1, 1, 1], self.zero_token_id, device=device, dtype=torch.long
|
| 415 |
+
)
|
| 416 |
+
special = torch.full_like(zero, self.initial_token_id)
|
| 417 |
+
|
| 418 |
+
text_special = torch.full_like(zero, self.text_initial_token_id)
|
| 419 |
+
audio_token = special
|
| 420 |
+
text_token = text_special
|
| 421 |
+
audio_token = audio_token.expand(-1, self.num_audio_codebooks, -1)
|
| 422 |
+
token = torch.cat([text_token, audio_token], dim=1)
|
| 423 |
+
return token
|
| 424 |
+
|
| 425 |
+
def embed_codes(self, sequence: torch.Tensor) -> torch.Tensor:
|
| 426 |
+
B, K, S = sequence.shape
|
| 427 |
+
assert (
|
| 428 |
+
K == self.num_codebooks
|
| 429 |
+
), f"Sequence shape {sequence.shape} must match the number of codebooks."
|
| 430 |
+
input_sequence = sequence
|
| 431 |
+
input_ = None
|
| 432 |
+
for cb_index in range(self.num_audio_codebooks):
|
| 433 |
+
audio_emb = self.emb[cb_index](
|
| 434 |
+
input_sequence[:, cb_index + self.audio_offset]
|
| 435 |
+
)
|
| 436 |
+
input_ = audio_emb if input_ is None else input_ + audio_emb
|
| 437 |
+
text_emb = self.text_emb(input_sequence[:, 0])
|
| 438 |
+
input_ = text_emb if input_ is None else input_ + text_emb
|
| 439 |
+
return input_
|
| 440 |
+
|
| 441 |
+
def forward_codes(
|
| 442 |
+
self,
|
| 443 |
+
sequence: torch.Tensor,
|
| 444 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 445 |
+
return self.forward_embeddings(self.embed_codes(sequence))
|
| 446 |
+
|
| 447 |
+
def forward_embeddings(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 448 |
+
# print("EMBED:", input[0, 0, :10].float().cpu().tolist()) # DEBUG
|
| 449 |
+
transformer_out = self.transformer(input)
|
| 450 |
+
if self.out_norm:
|
| 451 |
+
transformer_out = self.out_norm(transformer_out)
|
| 452 |
+
assert isinstance(transformer_out, torch.Tensor)
|
| 453 |
+
text_logits = self.text_linear(transformer_out)
|
| 454 |
+
text_logits = text_logits[:, None]
|
| 455 |
+
return transformer_out, text_logits
|
| 456 |
+
|
| 457 |
+
def forward_depformer(
|
| 458 |
+
self,
|
| 459 |
+
depformer_cb_index: int,
|
| 460 |
+
sequence: torch.Tensor,
|
| 461 |
+
transformer_out: torch.Tensor,
|
| 462 |
+
) -> torch.Tensor:
|
| 463 |
+
B, K, S = sequence.shape
|
| 464 |
+
assert (
|
| 465 |
+
K == 1
|
| 466 |
+
), f"Codebooks for Depformer streaming should be passed 1 by 1, got {K}."
|
| 467 |
+
assert (
|
| 468 |
+
S == 1
|
| 469 |
+
), f"Steps for Depformer streaming should be passed 1 by 1, got {S}."
|
| 470 |
+
assert (
|
| 471 |
+
transformer_out.shape[1] == 1
|
| 472 |
+
), "Transformer out should be a for a single step."
|
| 473 |
+
last_token_input: Optional[torch.Tensor] = None
|
| 474 |
+
depformer_input = transformer_out
|
| 475 |
+
if self.depformer_multi_linear:
|
| 476 |
+
depformer_input = self.depformer_in[depformer_cb_index](depformer_input)
|
| 477 |
+
else:
|
| 478 |
+
depformer_input = self.depformer_in[0](depformer_input)
|
| 479 |
+
if depformer_cb_index == 0:
|
| 480 |
+
last_token_input = self.depformer_text_emb(sequence[:, 0])
|
| 481 |
+
else:
|
| 482 |
+
last_token_input = self.depformer_emb[depformer_cb_index - 1](
|
| 483 |
+
sequence[:, 0]
|
| 484 |
+
)
|
| 485 |
+
depformer_input = depformer_input + last_token_input
|
| 486 |
+
assert depformer_input.shape[1] == 1
|
| 487 |
+
# depformer_input is [B, 1, depformer_dim].
|
| 488 |
+
# The streaming state of the depformer ensures that the proper layer is run.
|
| 489 |
+
dep_output = self.depformer(depformer_input)
|
| 490 |
+
logits = self.linears[depformer_cb_index](dep_output)
|
| 491 |
+
logits = logits[:, None]
|
| 492 |
+
assert logits.dim() == 4, logits.shape # [B, Ka, S, card]
|
| 493 |
+
return logits
|
| 494 |
+
|
| 495 |
+
def forward_depformer_training(
|
| 496 |
+
self,
|
| 497 |
+
sequence: torch.Tensor,
|
| 498 |
+
transformer_out: torch.Tensor,
|
| 499 |
+
) -> torch.Tensor:
|
| 500 |
+
B, K, T = sequence.shape
|
| 501 |
+
Ka = self.dep_q
|
| 502 |
+
assert (
|
| 503 |
+
K == self.num_codebooks
|
| 504 |
+
), f"Codebooks for Depformer training should be passed all at once, got {K,}."
|
| 505 |
+
depformer_inputs = []
|
| 506 |
+
for cb_index in range(Ka):
|
| 507 |
+
if self.depformer_multi_linear:
|
| 508 |
+
linear_index = cb_index
|
| 509 |
+
if self.depformer_weights_per_step_schedule is not None:
|
| 510 |
+
linear_index = self.depformer_weights_per_step_schedule[cb_index]
|
| 511 |
+
transformer_in = self.depformer_in[linear_index](transformer_out)
|
| 512 |
+
else:
|
| 513 |
+
transformer_in = self.depformer_in[0](transformer_out)
|
| 514 |
+
if cb_index == 0:
|
| 515 |
+
token_in = self.depformer_text_emb(sequence[:, 0])
|
| 516 |
+
else:
|
| 517 |
+
token_in = self.depformer_emb[cb_index - 1](sequence[:, cb_index + self.audio_offset - 1])
|
| 518 |
+
depformer_inputs.append(token_in + transformer_in)
|
| 519 |
+
depformer_input = torch.stack(depformer_inputs, 2)
|
| 520 |
+
# depformer_input is [B, T, K, depformer_dim], reshaping to [B * T, K, D]
|
| 521 |
+
depformer_input = depformer_input.view(B * T, Ka, -1)
|
| 522 |
+
depformer_output = self.depformer(depformer_input)
|
| 523 |
+
all_logits = []
|
| 524 |
+
for cb_index in range(Ka):
|
| 525 |
+
logits = self.linears[cb_index](depformer_output[:, cb_index])
|
| 526 |
+
all_logits.append(logits.view(B, T, -1))
|
| 527 |
+
logits = torch.stack(all_logits, 1)
|
| 528 |
+
assert logits.dim() == 4, logits.shape # [B, Ka, T, card]
|
| 529 |
+
return logits
|
| 530 |
+
|
| 531 |
+
def forward_train(self, codes: torch.Tensor):
|
| 532 |
+
B, K, T = codes.shape
|
| 533 |
+
# Delaying codes and removing the last time step that will never be an input.
|
| 534 |
+
initial = self._get_initial_token().expand(B, -1, -1)
|
| 535 |
+
delayed_codes = _delay_sequence(self.delays, codes, initial)
|
| 536 |
+
# Inserting the empty tokens for the first time step.
|
| 537 |
+
delayed_codes = torch.cat([initial, delayed_codes], dim=2)
|
| 538 |
+
|
| 539 |
+
# LLM Backbone
|
| 540 |
+
transformer_out, text_logits = self.forward_codes(delayed_codes[:, :, :-1])
|
| 541 |
+
logits = self.forward_depformer_training(delayed_codes[:, :, 1:], transformer_out)
|
| 542 |
+
|
| 543 |
+
# map back the logits on pattern sequence to logits on original codes: [B, K, S, card] -> [B, K, T, card]
|
| 544 |
+
# and provide the corresponding mask over invalid positions of tokens. We will with NaN values invalid positions
|
| 545 |
+
# to ensure they properly handled.
|
| 546 |
+
logits, logits_mask = _undelay_sequence(
|
| 547 |
+
self.delays[self.audio_offset:self.audio_offset + self.dep_q],
|
| 548 |
+
logits, fill_value=float('NaN'))
|
| 549 |
+
logits_mask &= (codes[:, self.audio_offset: self.audio_offset + self.dep_q] != self.zero_token_id)
|
| 550 |
+
text_logits, text_logits_mask = _undelay_sequence(self.delays[:1], text_logits, fill_value=float('NaN'))
|
| 551 |
+
text_logits_mask &= (codes[:, :1] != self.zero_token_id)
|
| 552 |
+
return LMOutput(logits, logits_mask, text_logits, text_logits_mask)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
@dataclass
|
| 556 |
+
class _LMGenState:
|
| 557 |
+
cache: torch.Tensor
|
| 558 |
+
provided: torch.Tensor
|
| 559 |
+
initial: torch.Tensor
|
| 560 |
+
graphed_main: CUDAGraphed
|
| 561 |
+
graphed_embeddings: CUDAGraphed
|
| 562 |
+
graphed_depth: CUDAGraphed
|
| 563 |
+
offset: int = 0
|
| 564 |
+
|
| 565 |
+
def reset(self):
|
| 566 |
+
self.offset = 0
|
| 567 |
+
self.provided[:] = False
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
@torch.no_grad()
|
| 571 |
+
def create_loss_report(
|
| 572 |
+
state_cache: torch.Tensor,
|
| 573 |
+
lm_model: LMModel,
|
| 574 |
+
text_logits: torch.Tensor,
|
| 575 |
+
audio_logits: torch.Tensor,
|
| 576 |
+
target: torch.Tensor,
|
| 577 |
+
sampled_text_token: torch.Tensor,
|
| 578 |
+
sampled_audio_tokens: torch.Tensor,
|
| 579 |
+
target_position: int,
|
| 580 |
+
) -> dict[str, torch.Tensor]:
|
| 581 |
+
report = {}
|
| 582 |
+
B = state_cache.shape[0]
|
| 583 |
+
# model_tokens is the sampled output from model_logits
|
| 584 |
+
model_tokens = torch.zeros_like(state_cache[:, :, target_position])
|
| 585 |
+
model_tokens[:, 0] = sampled_text_token
|
| 586 |
+
model_tokens[:, 1 : lm_model.dep_q + 1] = sampled_audio_tokens
|
| 587 |
+
|
| 588 |
+
report.update(
|
| 589 |
+
{
|
| 590 |
+
"forced_tokens": torch.zeros((B, lm_model.dep_q + 1)),
|
| 591 |
+
"model_tokens": torch.zeros((B, lm_model.dep_q + 1)),
|
| 592 |
+
"ranks_of_forced": torch.zeros((B, lm_model.dep_q + 1)),
|
| 593 |
+
"losses": torch.zeros((B, lm_model.dep_q+1)),
|
| 594 |
+
}
|
| 595 |
+
)
|
| 596 |
+
report["model_tokens"] = model_tokens.clone()
|
| 597 |
+
report["forced_tokens"] = target.clone()
|
| 598 |
+
|
| 599 |
+
# Text Channel
|
| 600 |
+
text_logits = text_logits.squeeze(dim=1).squeeze(dim=1)
|
| 601 |
+
target = target[:, 0].squeeze(1).clone()
|
| 602 |
+
|
| 603 |
+
text_probs = torch.softmax(text_logits, dim=-1)
|
| 604 |
+
text_ranks = torch.argsort(text_probs, dim=-1, descending=True)
|
| 605 |
+
for b in range(B):
|
| 606 |
+
forced_token = target[b].item()
|
| 607 |
+
try:
|
| 608 |
+
rank = (text_ranks[b] == forced_token).nonzero().item()
|
| 609 |
+
except RuntimeError:
|
| 610 |
+
rank = lm_model.zero_token_id
|
| 611 |
+
report["ranks_of_forced"][b, 0] = rank
|
| 612 |
+
|
| 613 |
+
target[target == lm_model.text_initial_token_id] = -100
|
| 614 |
+
text_loss = torch.nn.functional.cross_entropy(
|
| 615 |
+
text_logits,
|
| 616 |
+
target,
|
| 617 |
+
ignore_index=-100,
|
| 618 |
+
)
|
| 619 |
+
report["losses"][:, 0] = text_loss
|
| 620 |
+
|
| 621 |
+
# Audio Channels
|
| 622 |
+
for k in range(lm_model.dep_q):
|
| 623 |
+
target = target[:, k+1].squeeze(1).clone()
|
| 624 |
+
channel_logits = audio_logits[:, k, :]
|
| 625 |
+
|
| 626 |
+
audio_probs = torch.softmax(channel_logits, dim=-1)
|
| 627 |
+
audio_ranks = torch.argsort(audio_probs, dim=-1, descending=True)
|
| 628 |
+
for b in range(B):
|
| 629 |
+
forced_token = target[b].item()
|
| 630 |
+
try:
|
| 631 |
+
rank = (audio_ranks[b] == forced_token).nonzero().item()
|
| 632 |
+
except RuntimeError:
|
| 633 |
+
rank = lm_model.zero_token_id
|
| 634 |
+
report["ranks_of_forced"][b, k + 1] = rank
|
| 635 |
+
|
| 636 |
+
target[target == lm_model.initial_token_id] = -100
|
| 637 |
+
audio_loss = torch.nn.functional.cross_entropy(
|
| 638 |
+
channel_logits,
|
| 639 |
+
target,
|
| 640 |
+
ignore_index=-100,
|
| 641 |
+
)
|
| 642 |
+
report["losses"][:, k + 1] = audio_loss
|
| 643 |
+
return report
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
class LMGen(StreamingModule[_LMGenState]):
|
| 647 |
+
def __init__(
|
| 648 |
+
self,
|
| 649 |
+
lm_model: LMModel,
|
| 650 |
+
device: str | torch.device,
|
| 651 |
+
use_sampling: bool = True,
|
| 652 |
+
temp: float = 0.8,
|
| 653 |
+
temp_text: float = 0.7,
|
| 654 |
+
top_k: int = 250,
|
| 655 |
+
top_k_text: int = 25,
|
| 656 |
+
check: bool = False,
|
| 657 |
+
report_loss: bool = False,
|
| 658 |
+
return_logits: bool = False,
|
| 659 |
+
audio_silence_frame_cnt: int = 1,
|
| 660 |
+
text_prompt_tokens: Optional[list[int]] = None,
|
| 661 |
+
save_voice_prompt_embeddings: bool = False,
|
| 662 |
+
sample_rate: int = 32000,
|
| 663 |
+
frame_rate: int = FRAME_RATE_HZ,
|
| 664 |
+
):
|
| 665 |
+
assert not lm_model.training, "generation shouldn't be used in training mode."
|
| 666 |
+
super().__init__()
|
| 667 |
+
|
| 668 |
+
self.lm_model = lm_model
|
| 669 |
+
self.use_sampling = use_sampling
|
| 670 |
+
self.temp = temp
|
| 671 |
+
self.temp_text = temp_text
|
| 672 |
+
self.top_k = top_k
|
| 673 |
+
self.top_k_text = top_k_text
|
| 674 |
+
self.text_prompt_tokens = text_prompt_tokens
|
| 675 |
+
self.audio_silence_frame_cnt = audio_silence_frame_cnt
|
| 676 |
+
self.voice_prompt = None
|
| 677 |
+
self.zero_text_code = 3
|
| 678 |
+
self._frame_rate = frame_rate
|
| 679 |
+
self._sample_rate = sample_rate
|
| 680 |
+
self._frame_size = int(self._sample_rate / self._frame_rate)
|
| 681 |
+
self._zero_frame = torch.zeros(1, 1, self._frame_size, device=device)
|
| 682 |
+
duration = self._frame_size / self._sample_rate
|
| 683 |
+
sine = create_sinewave(duration, self._sample_rate)
|
| 684 |
+
self._sine_frame = torch.tensor(sine, device=device).unsqueeze(0).unsqueeze(0) # (1,1,T)
|
| 685 |
+
self.check = check
|
| 686 |
+
self.report_loss = report_loss
|
| 687 |
+
if report_loss:
|
| 688 |
+
return_logits = True
|
| 689 |
+
self.return_logits = return_logits
|
| 690 |
+
self.max_delay = max(
|
| 691 |
+
lm_model.delays
|
| 692 |
+
) # with delays, we need to generate a few more time steps.
|
| 693 |
+
self.delays_cuda = torch.tensor(
|
| 694 |
+
lm_model.delays, device=lm_model.device, dtype=torch.long
|
| 695 |
+
)
|
| 696 |
+
self.save_voice_prompt_embeddings = save_voice_prompt_embeddings
|
| 697 |
+
self.voice_prompt_audio: Optional[torch.Tensor] = None
|
| 698 |
+
self.voice_prompt_cache: Optional[torch.Tensor] = None
|
| 699 |
+
self.voice_prompt_embeddings: Optional[torch.Tensor] = None
|
| 700 |
+
#self.voice_prompt_mimi_streaming_state: Optional[StreamingStateDict] = None
|
| 701 |
+
|
| 702 |
+
def _init_streaming_state(self, batch_size: int) -> _LMGenState:
|
| 703 |
+
lm_model = self.lm_model
|
| 704 |
+
initial = lm_model._get_initial_token()
|
| 705 |
+
cache = torch.full(
|
| 706 |
+
(batch_size, self.lm_model.num_codebooks, self.max_delay + 3),
|
| 707 |
+
lm_model.ungenerated_token_id,
|
| 708 |
+
device=lm_model.device,
|
| 709 |
+
dtype=torch.long,
|
| 710 |
+
)
|
| 711 |
+
provided = torch.full(
|
| 712 |
+
(batch_size, self.lm_model.num_codebooks, self.max_delay + 3),
|
| 713 |
+
False,
|
| 714 |
+
device=lm_model.device,
|
| 715 |
+
dtype=torch.bool
|
| 716 |
+
)
|
| 717 |
+
|
| 718 |
+
disable = lm_model.device.type != 'cuda'
|
| 719 |
+
# disable = True # DEBUG
|
| 720 |
+
graphed_main = CUDAGraphed(lm_model.forward_codes, disable=disable)
|
| 721 |
+
graphed_embeddings = CUDAGraphed(lm_model.forward_embeddings, disable=disable)
|
| 722 |
+
graphed_depth = CUDAGraphed(self.depformer_step, disable=disable)
|
| 723 |
+
|
| 724 |
+
return _LMGenState(cache, provided, initial, graphed_main, graphed_embeddings, graphed_depth)
|
| 725 |
+
|
| 726 |
+
@torch.no_grad()
|
| 727 |
+
def prepare_step_input(self,
|
| 728 |
+
input_tokens: torch.Tensor=None,
|
| 729 |
+
moshi_tokens:torch.Tensor=None,
|
| 730 |
+
text_token:torch.Tensor=None,
|
| 731 |
+
):
|
| 732 |
+
state = self._streaming_state
|
| 733 |
+
if state is None:
|
| 734 |
+
raise RuntimeError(
|
| 735 |
+
"You should wrap those calls with a `with lm_gen.streaming(): ...`."
|
| 736 |
+
)
|
| 737 |
+
lm_model = self.lm_model
|
| 738 |
+
|
| 739 |
+
# audio_tokens_per_stream = lm_model.dep_q//2
|
| 740 |
+
needed_tokens = lm_model.num_codebooks - AUDIO_TOKENS_PER_STREAM - 1
|
| 741 |
+
CT = state.cache.shape[2]
|
| 742 |
+
|
| 743 |
+
####
|
| 744 |
+
# Fill Cache with provided tokens at state.offset (target) + delays
|
| 745 |
+
|
| 746 |
+
if input_tokens is not None:
|
| 747 |
+
assert input_tokens.dim() == 3, "Shape should be [B, K, T]."
|
| 748 |
+
B, Ki, S = input_tokens.shape
|
| 749 |
+
assert S == 1, "Only support being given steps one by one."
|
| 750 |
+
assert (
|
| 751 |
+
Ki == needed_tokens
|
| 752 |
+
), f"We expect {needed_tokens} tokens from the user stream, got {Ki}."
|
| 753 |
+
|
| 754 |
+
for q_other in range(input_tokens.shape[1]):
|
| 755 |
+
k = AUDIO_TOKENS_PER_STREAM + 1 + q_other
|
| 756 |
+
delay = lm_model.delays[k]
|
| 757 |
+
write_position = (state.offset + delay) % CT
|
| 758 |
+
state.cache[:, k, write_position : write_position + 1] = input_tokens[:, q_other]
|
| 759 |
+
state.provided[:, k, write_position : write_position + 1] = True
|
| 760 |
+
|
| 761 |
+
if moshi_tokens is not None:
|
| 762 |
+
assert moshi_tokens.dim() == 3, "Shape should be [B, K, T]."
|
| 763 |
+
B, Ki, S = moshi_tokens.shape
|
| 764 |
+
assert S == 1, "Only support being given steps one by one."
|
| 765 |
+
assert (
|
| 766 |
+
Ki == needed_tokens
|
| 767 |
+
), f"We expect {needed_tokens} tokens from the moshi stream, got {Ki}."
|
| 768 |
+
|
| 769 |
+
for q_moshi in range(moshi_tokens.shape[1]):
|
| 770 |
+
k = 1 + q_moshi
|
| 771 |
+
delay = lm_model.delays[k]
|
| 772 |
+
write_position = (state.offset + delay) % CT
|
| 773 |
+
state.cache[:, k, write_position : write_position + 1] = moshi_tokens[:, q_moshi]
|
| 774 |
+
state.provided[:, k, write_position : write_position + 1] = True
|
| 775 |
+
|
| 776 |
+
if text_token is not None:
|
| 777 |
+
write_position = (state.offset + lm_model.delays[0]) % CT
|
| 778 |
+
state.cache[:, 0, write_position] = text_token
|
| 779 |
+
state.provided[:, 0, write_position] = True
|
| 780 |
+
|
| 781 |
+
for k, delay in enumerate(lm_model.delays):
|
| 782 |
+
# Only for the very beginning, we extend the initial token for the acoustic
|
| 783 |
+
# token that are delayed, and thus have no good value to take.
|
| 784 |
+
if state.offset <= delay:
|
| 785 |
+
state.cache[:, k, state.offset % CT] = state.initial[:, k, 0]
|
| 786 |
+
state.provided[:, k, state.offset % CT] = True
|
| 787 |
+
|
| 788 |
+
####
|
| 789 |
+
# Perform inference at state.offset - 1 (model_input); forcing with tokens at state.offset (target) when provided
|
| 790 |
+
|
| 791 |
+
if state.offset == 0:
|
| 792 |
+
# We can't report loss or force depth tranformer tokens until we're at step 2
|
| 793 |
+
# And we need to initialize the delay-0 cache where it's not provided for step 2
|
| 794 |
+
state.cache[:, :, 0] = state.initial[:, :, 0] # torch.where(state.provided[:, :, 0], state.cache[:, :, 0], state.initial[:, :, 0])
|
| 795 |
+
state.offset += 1
|
| 796 |
+
return None
|
| 797 |
+
|
| 798 |
+
model_input_position = (state.offset-1) % CT
|
| 799 |
+
target_position = state.offset % CT
|
| 800 |
+
input_ = state.cache[:, :, model_input_position : model_input_position + 1]
|
| 801 |
+
target_ = state.cache[:, :, target_position : target_position + 1]
|
| 802 |
+
provided_ = state.provided[:, :, target_position : target_position + 1]
|
| 803 |
+
|
| 804 |
+
if self.check:
|
| 805 |
+
# Check that we are not feeding in any value that is not generated yet.
|
| 806 |
+
assert not (input_ == lm_model.ungenerated_token_id).any(), (
|
| 807 |
+
state.offset,
|
| 808 |
+
input_,
|
| 809 |
+
)
|
| 810 |
+
assert (input_[:, lm_model.audio_offset :] <= lm_model.card).all(), input_
|
| 811 |
+
assert (input_[:, :1] <= lm_model.text_card).all()
|
| 812 |
+
return input_, provided_, target_, model_input_position, target_position
|
| 813 |
+
|
| 814 |
+
@torch.no_grad()
|
| 815 |
+
def step(self, input_tokens: torch.Tensor=None, moshi_tokens:torch.Tensor=None, text_token:torch.Tensor=None,
|
| 816 |
+
return_embeddings: bool=False) \
|
| 817 |
+
-> torch.Tensor | tuple[torch.Tensor, torch.Tensor] | tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
| 818 |
+
state = self._streaming_state
|
| 819 |
+
lm_model = self.lm_model
|
| 820 |
+
prepared_inputs = self.prepare_step_input(
|
| 821 |
+
input_tokens, moshi_tokens, text_token,
|
| 822 |
+
)
|
| 823 |
+
# print("INPUT:", None if input_tokens is None else input_tokens.squeeze().cpu().tolist()) # DEBUG
|
| 824 |
+
# print("MOSHI:", None if moshi_tokens is None else moshi_tokens.squeeze().cpu().tolist()) # DEBUG
|
| 825 |
+
if prepared_inputs is None:
|
| 826 |
+
return (None, None) if self.report_loss or self.return_logits else None
|
| 827 |
+
input_, provided_, target_, model_input_position, target_position = prepared_inputs
|
| 828 |
+
if self.check:
|
| 829 |
+
# Check that we are not feeding in any value that is not generated yet.
|
| 830 |
+
assert not (input_ == lm_model.ungenerated_token_id).any(), (
|
| 831 |
+
state.offset,
|
| 832 |
+
input_,
|
| 833 |
+
)
|
| 834 |
+
assert (input_[:, lm_model.audio_offset :] <= lm_model.card).all(), input_
|
| 835 |
+
assert (input_[:, :1] <= lm_model.text_card).all()
|
| 836 |
+
embeddings = None
|
| 837 |
+
if return_embeddings:
|
| 838 |
+
embeddings = self.lm_model.embed_codes(input_)
|
| 839 |
+
transformer_out, text_logits = state.graphed_main(input_)
|
| 840 |
+
output = self.process_transformer_output(
|
| 841 |
+
transformer_out,
|
| 842 |
+
text_logits,
|
| 843 |
+
provided_,
|
| 844 |
+
target_,
|
| 845 |
+
model_input_position,
|
| 846 |
+
target_position,
|
| 847 |
+
)
|
| 848 |
+
if return_embeddings:
|
| 849 |
+
return output, embeddings
|
| 850 |
+
return output
|
| 851 |
+
|
| 852 |
+
@torch.no_grad()
|
| 853 |
+
def step_embeddings(self, embeddings: torch.Tensor):
|
| 854 |
+
state = self._streaming_state
|
| 855 |
+
lm_model = self.lm_model
|
| 856 |
+
needed_input_tokens = lm_model.num_codebooks - AUDIO_TOKENS_PER_STREAM - 1
|
| 857 |
+
_dummy_audio_token = lm_model._get_initial_token()
|
| 858 |
+
while True:
|
| 859 |
+
prepared_inputs = self.prepare_step_input(
|
| 860 |
+
input_tokens=_dummy_audio_token[:, 1:1+needed_input_tokens], moshi_tokens=_dummy_audio_token[:, 1+needed_input_tokens:], text_token=self.zero_text_code,
|
| 861 |
+
)
|
| 862 |
+
if prepared_inputs is not None:
|
| 863 |
+
break
|
| 864 |
+
_, provided_, target_, model_input_position, target_position = prepared_inputs
|
| 865 |
+
transformer_out, text_logits = state.graphed_embeddings(embeddings)
|
| 866 |
+
return self.process_transformer_output(
|
| 867 |
+
transformer_out,
|
| 868 |
+
text_logits,
|
| 869 |
+
provided_,
|
| 870 |
+
target_,
|
| 871 |
+
model_input_position,
|
| 872 |
+
target_position,
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
@torch.no_grad()
|
| 876 |
+
def process_transformer_output(self, transformer_out, text_logits, provided_, target_, model_input_position, target_position):
|
| 877 |
+
state = self._streaming_state
|
| 878 |
+
lm_model = self.lm_model
|
| 879 |
+
|
| 880 |
+
# Shape of text_logits should be [B, K_text=1, T=1, Card_text]
|
| 881 |
+
sampled_text_token = sample_token(
|
| 882 |
+
text_logits.float(),
|
| 883 |
+
self.use_sampling,
|
| 884 |
+
self.temp_text,
|
| 885 |
+
self.top_k_text,
|
| 886 |
+
)
|
| 887 |
+
assert sampled_text_token.dim() == 3, sampled_text_token.shape
|
| 888 |
+
assert sampled_text_token.shape[2] == 1
|
| 889 |
+
assert sampled_text_token.shape[1] == 1, "Only one text stream supported."
|
| 890 |
+
sampled_text_token = sampled_text_token[:, 0, 0] # shape is [B]
|
| 891 |
+
|
| 892 |
+
next_text_token = torch.where(provided_[:, 0, 0], target_[:, 0, 0], sampled_text_token)
|
| 893 |
+
|
| 894 |
+
if self.return_logits:
|
| 895 |
+
sampled_audio_tokens, audio_logits = state.graphed_depth(next_text_token, transformer_out, target_[:,lm_model.audio_offset:,0], provided_[:,lm_model.audio_offset:,0]) # [B, K_audio, Card_audio]
|
| 896 |
+
else:
|
| 897 |
+
sampled_audio_tokens = state.graphed_depth(next_text_token, transformer_out, target_[:,lm_model.audio_offset:,0], provided_[:,lm_model.audio_offset:,0])
|
| 898 |
+
|
| 899 |
+
state.provided[:, :, model_input_position] = False
|
| 900 |
+
####
|
| 901 |
+
# Fill cache with generated tokens at state.offset (where not provided)
|
| 902 |
+
|
| 903 |
+
state.cache[:, 0, target_position] = torch.where(
|
| 904 |
+
~state.provided[:, 0, target_position],
|
| 905 |
+
sampled_text_token,
|
| 906 |
+
state.cache[:, 0, target_position],
|
| 907 |
+
)
|
| 908 |
+
state.cache[:, 1 : lm_model.dep_q + 1, target_position] = torch.where(
|
| 909 |
+
~state.provided[:, 1 : lm_model.dep_q + 1, target_position],
|
| 910 |
+
sampled_audio_tokens,
|
| 911 |
+
state.cache[:, 1 : lm_model.dep_q + 1, target_position],
|
| 912 |
+
)
|
| 913 |
+
|
| 914 |
+
####
|
| 915 |
+
# Calculate loss of model logits (based on state.offset - 1) compared to target (state.offset)
|
| 916 |
+
|
| 917 |
+
report = {}
|
| 918 |
+
if self.report_loss:
|
| 919 |
+
report = create_loss_report(
|
| 920 |
+
state_cache=state.cache,
|
| 921 |
+
lm_model=lm_model,
|
| 922 |
+
text_logits=text_logits,
|
| 923 |
+
audio_logits=audio_logits,
|
| 924 |
+
target=target_,
|
| 925 |
+
sampled_text_token=sampled_text_token,
|
| 926 |
+
sampled_audio_tokens=sampled_audio_tokens,
|
| 927 |
+
target_position=target_position,
|
| 928 |
+
)
|
| 929 |
+
|
| 930 |
+
####
|
| 931 |
+
# Collect outputs for state.offset - max_delay
|
| 932 |
+
|
| 933 |
+
if state.offset <= self.max_delay:
|
| 934 |
+
state.offset += 1
|
| 935 |
+
if self.report_loss:
|
| 936 |
+
return None, report
|
| 937 |
+
if self.return_logits:
|
| 938 |
+
return None, None
|
| 939 |
+
else:
|
| 940 |
+
return None
|
| 941 |
+
|
| 942 |
+
B = state.cache.shape[0]
|
| 943 |
+
CT = state.cache.shape[2]
|
| 944 |
+
gen_delays_cuda = self.delays_cuda[: lm_model.dep_q + 1]
|
| 945 |
+
index = (
|
| 946 |
+
((state.offset - self.max_delay + gen_delays_cuda) % CT)
|
| 947 |
+
.view(1, -1, 1)
|
| 948 |
+
.expand(B, -1, 1)
|
| 949 |
+
)
|
| 950 |
+
out = state.cache.gather(dim=2, index=index)
|
| 951 |
+
|
| 952 |
+
state.offset += 1
|
| 953 |
+
if self.report_loss:
|
| 954 |
+
return out, report
|
| 955 |
+
elif self.return_logits and not self.report_loss:
|
| 956 |
+
return out, (text_logits.clone(), audio_logits.clone())
|
| 957 |
+
else:
|
| 958 |
+
return out
|
| 959 |
+
|
| 960 |
+
def load_voice_prompt(self, voice_prompt: str):
|
| 961 |
+
self.voice_prompt = voice_prompt
|
| 962 |
+
raw_audio = load_audio(
|
| 963 |
+
voice_prompt, self._sample_rate,
|
| 964 |
+
) # shape: (1, T) for mono
|
| 965 |
+
|
| 966 |
+
# Normalize to -24 LUFS (mono-safe)
|
| 967 |
+
raw_audio = normalize_audio(raw_audio, self._sample_rate, -24.0)
|
| 968 |
+
|
| 969 |
+
# Keep shape (1, T) because your encoder expects channels-first
|
| 970 |
+
if raw_audio.ndim == 1:
|
| 971 |
+
raw_audio = raw_audio[None, :]
|
| 972 |
+
|
| 973 |
+
self.voice_prompt_audio = raw_audio
|
| 974 |
+
self.voice_prompt_cache: Optional[torch.Tensor] = None
|
| 975 |
+
self.voice_prompt_embeddings: Optional[torch.Tensor] = None
|
| 976 |
+
|
| 977 |
+
def load_voice_prompt_embeddings(self, path: str):
|
| 978 |
+
self.voice_prompt = path
|
| 979 |
+
state = torch.load(path)
|
| 980 |
+
|
| 981 |
+
self.voice_prompt_audio = None
|
| 982 |
+
self.voice_prompt_embeddings = state["embeddings"].to(self.lm_model.device)
|
| 983 |
+
self.voice_prompt_cache = state["cache"].to(self.lm_model.device)
|
| 984 |
+
|
| 985 |
+
def _encode_zero_frame(self) -> torch.Tensor:
|
| 986 |
+
return torch.as_tensor(
|
| 987 |
+
SILENCE_TOKENS,
|
| 988 |
+
dtype=torch.long,
|
| 989 |
+
device=self.lm_model.device,
|
| 990 |
+
).view(1, 8, 1)
|
| 991 |
+
|
| 992 |
+
def _encode_sine_frame(self) -> torch.Tensor:
|
| 993 |
+
return torch.as_tensor(
|
| 994 |
+
SINE_TOKENS,
|
| 995 |
+
dtype=torch.long,
|
| 996 |
+
device=self.lm_model.device,
|
| 997 |
+
).view(1, 8, 1)
|
| 998 |
+
|
| 999 |
+
def _encode_voice_prompt_frames(self, mimi):
|
| 1000 |
+
return encode_from_sphn(
|
| 1001 |
+
mimi,
|
| 1002 |
+
_iterate_audio(
|
| 1003 |
+
self.voice_prompt_audio,
|
| 1004 |
+
sample_interval_size=self._frame_size,
|
| 1005 |
+
pad=True,
|
| 1006 |
+
),
|
| 1007 |
+
max_batch=1,
|
| 1008 |
+
)
|
| 1009 |
+
|
| 1010 |
+
def _step_voice_prompt_frame(self,
|
| 1011 |
+
voice_prompt_frame_tokens: torch.Tensor,
|
| 1012 |
+
saved_embeddings: Optional[list[torch.Tensor]]=None,
|
| 1013 |
+
):
|
| 1014 |
+
# Always use zero_text_code during voice prompt
|
| 1015 |
+
out = self.step(
|
| 1016 |
+
moshi_tokens=voice_prompt_frame_tokens,
|
| 1017 |
+
text_token=self.zero_text_code,
|
| 1018 |
+
input_tokens=self._encode_sine_frame(),
|
| 1019 |
+
return_embeddings=self.save_voice_prompt_embeddings,
|
| 1020 |
+
)
|
| 1021 |
+
if out is not None and self.save_voice_prompt_embeddings:
|
| 1022 |
+
_, embeddings = out
|
| 1023 |
+
saved_embeddings.append(embeddings)
|
| 1024 |
+
|
| 1025 |
+
def _step_voice_prompt_core(self, mimi) -> Iterator[None]:
|
| 1026 |
+
"""Shared core for stepping through the voice prompt.
|
| 1027 |
+
|
| 1028 |
+
This generator yields at each *checkpoint* where the async wrapper may want to
|
| 1029 |
+
consult `is_alive`. The core itself is intentionally unaware of connection state.
|
| 1030 |
+
"""
|
| 1031 |
+
if self.voice_prompt_embeddings is not None:
|
| 1032 |
+
# Replay stored voice prompt embeddings
|
| 1033 |
+
for next_embed in self.voice_prompt_embeddings:
|
| 1034 |
+
yield
|
| 1035 |
+
self.step_embeddings(next_embed)
|
| 1036 |
+
|
| 1037 |
+
state = self._streaming_state
|
| 1038 |
+
state.cache.copy_(self.voice_prompt_cache)
|
| 1039 |
+
return
|
| 1040 |
+
|
| 1041 |
+
elif self.voice_prompt_audio is not None:
|
| 1042 |
+
saved_embeddings = []
|
| 1043 |
+
for voice_prompt_frame_tokens in self._encode_voice_prompt_frames(mimi):
|
| 1044 |
+
yield
|
| 1045 |
+
self._step_voice_prompt_frame(
|
| 1046 |
+
voice_prompt_frame_tokens,
|
| 1047 |
+
saved_embeddings
|
| 1048 |
+
)
|
| 1049 |
+
# One last checkpoint before any optional save (nice-to-have for async disconnect)
|
| 1050 |
+
yield
|
| 1051 |
+
|
| 1052 |
+
if self.save_voice_prompt_embeddings:
|
| 1053 |
+
# Offset int(self._streaming_state.offset) is not needed since calling step() for len(voice_prompt_frame_tokens)
|
| 1054 |
+
# and calling step_embeddings() for len(voice_prompt_embeddings) will increment offset by the same amount
|
| 1055 |
+
torch.save(
|
| 1056 |
+
{
|
| 1057 |
+
"embeddings": torch.stack(saved_embeddings, dim=0).detach().cpu(),
|
| 1058 |
+
"cache": self._streaming_state.cache
|
| 1059 |
+
},
|
| 1060 |
+
splitext(self.voice_prompt)[0] + ".pt",
|
| 1061 |
+
)
|
| 1062 |
+
print('Done loading voice prompt.')
|
| 1063 |
+
|
| 1064 |
+
def _step_voice_prompt(self, mimi):
|
| 1065 |
+
# Sync path intentionally does not support `is_alive` / disconnect checks.
|
| 1066 |
+
for _ in self._step_voice_prompt_core(mimi):
|
| 1067 |
+
pass
|
| 1068 |
+
|
| 1069 |
+
async def _step_voice_prompt_async(self, mimi, is_alive: Optional[Callable]=None):
|
| 1070 |
+
for _ in self._step_voice_prompt_core(mimi):
|
| 1071 |
+
if is_alive is not None and not await is_alive():
|
| 1072 |
+
break
|
| 1073 |
+
|
| 1074 |
+
def _step_audio_silence_core(self) -> Iterator[None]:
|
| 1075 |
+
# For slots of silence (default 0.5s) after voice/text prompts
|
| 1076 |
+
# (agent text, user audio, agent audio) : (PADs, silence, sine)
|
| 1077 |
+
for _ in range(self.audio_silence_frame_cnt):
|
| 1078 |
+
yield
|
| 1079 |
+
self.step(
|
| 1080 |
+
moshi_tokens=self._encode_zero_frame(),
|
| 1081 |
+
text_token=self.zero_text_code,
|
| 1082 |
+
input_tokens=self._encode_sine_frame(),
|
| 1083 |
+
)
|
| 1084 |
+
print('Done loading audio silence.')
|
| 1085 |
+
|
| 1086 |
+
def _step_audio_silence(self):
|
| 1087 |
+
# Sync path intentionally does not support `is_alive` / disconnect checks.
|
| 1088 |
+
for _ in self._step_audio_silence_core():
|
| 1089 |
+
pass
|
| 1090 |
+
|
| 1091 |
+
async def _step_audio_silence_async(self, is_alive: Optional[Callable]=None):
|
| 1092 |
+
for _ in self._step_audio_silence_core():
|
| 1093 |
+
if is_alive is not None and not await is_alive():
|
| 1094 |
+
break
|
| 1095 |
+
|
| 1096 |
+
def _step_text_prompt_core(self) -> Iterator[None]:
|
| 1097 |
+
for text_prompt_token in self.text_prompt_tokens:
|
| 1098 |
+
yield
|
| 1099 |
+
self.step(
|
| 1100 |
+
moshi_tokens=self._encode_zero_frame(),
|
| 1101 |
+
text_token=text_prompt_token,
|
| 1102 |
+
input_tokens=self._encode_sine_frame(),
|
| 1103 |
+
)
|
| 1104 |
+
print('Done loading text prompt.')
|
| 1105 |
+
|
| 1106 |
+
|
| 1107 |
+
def _step_text_prompt(self):
|
| 1108 |
+
# Sync path intentionally does not support `is_alive` / disconnect checks.
|
| 1109 |
+
for _ in self._step_text_prompt_core():
|
| 1110 |
+
pass
|
| 1111 |
+
|
| 1112 |
+
async def _step_text_prompt_async(self, is_alive: Optional[Callable]=None):
|
| 1113 |
+
for _ in self._step_text_prompt_core():
|
| 1114 |
+
if is_alive is not None and not await is_alive():
|
| 1115 |
+
break
|
| 1116 |
+
|
| 1117 |
+
async def step_system_prompts_async(self, mimi, is_alive: Optional[Callable]=None):
|
| 1118 |
+
await self._step_voice_prompt_async(mimi, is_alive)
|
| 1119 |
+
await self._step_audio_silence_async(is_alive)
|
| 1120 |
+
await self._step_text_prompt_async(is_alive)
|
| 1121 |
+
await self._step_audio_silence_async(is_alive)
|
| 1122 |
+
|
| 1123 |
+
def step_system_prompts(self, mimi):
|
| 1124 |
+
self._step_voice_prompt(mimi)
|
| 1125 |
+
self._step_audio_silence()
|
| 1126 |
+
self._step_text_prompt()
|
| 1127 |
+
self._step_audio_silence()
|
| 1128 |
+
|
| 1129 |
+
def depformer_step(
|
| 1130 |
+
self,
|
| 1131 |
+
text_token: torch.Tensor,
|
| 1132 |
+
transformer_out: torch.Tensor,
|
| 1133 |
+
audio_tokens: torch.Tensor,
|
| 1134 |
+
audio_provided: torch.Tensor
|
| 1135 |
+
) -> torch.Tensor:
|
| 1136 |
+
(B,) = text_token.shape
|
| 1137 |
+
prev_token = text_token
|
| 1138 |
+
lm_model = self.lm_model
|
| 1139 |
+
depformer_tokens: list[torch.Tensor] = []
|
| 1140 |
+
depformer_logits: list[torch.Tensor] = []
|
| 1141 |
+
assert not lm_model.depformer.is_streaming
|
| 1142 |
+
with lm_model.depformer.streaming(B):
|
| 1143 |
+
for cb_index in range(lm_model.dep_q):
|
| 1144 |
+
input_ = prev_token[:, None, None]
|
| 1145 |
+
logits = lm_model.forward_depformer(cb_index, input_, transformer_out)
|
| 1146 |
+
if self.return_logits:
|
| 1147 |
+
assert logits.shape == (B, 1, 1, lm_model.card), logits.shape
|
| 1148 |
+
ret_logits = logits.squeeze(dim=1).squeeze(dim=1)
|
| 1149 |
+
assert ret_logits.shape == (B, lm_model.card), ret_logits.shape
|
| 1150 |
+
depformer_logits.append(ret_logits.float())
|
| 1151 |
+
next_token = sample_token(
|
| 1152 |
+
logits.float(),
|
| 1153 |
+
self.use_sampling,
|
| 1154 |
+
self.temp,
|
| 1155 |
+
self.top_k,
|
| 1156 |
+
)
|
| 1157 |
+
assert next_token.shape == (B, 1, 1)
|
| 1158 |
+
next_token = next_token[:, 0, 0] # shape is B
|
| 1159 |
+
prev_token = torch.where(
|
| 1160 |
+
audio_provided[:, cb_index],
|
| 1161 |
+
audio_tokens[:, cb_index],
|
| 1162 |
+
next_token,
|
| 1163 |
+
)
|
| 1164 |
+
depformer_tokens.append(next_token)
|
| 1165 |
+
|
| 1166 |
+
assert len(depformer_tokens) == lm_model.dep_q, (
|
| 1167 |
+
len(depformer_tokens),
|
| 1168 |
+
lm_model.dep_q,
|
| 1169 |
+
)
|
| 1170 |
+
tokens = torch.stack(depformer_tokens, dim=1)
|
| 1171 |
+
assert tokens.shape == (B, lm_model.dep_q), tokens.shape
|
| 1172 |
+
if self.return_logits:
|
| 1173 |
+
all_logits = torch.stack(depformer_logits, dim=1)
|
| 1174 |
+
assert all_logits.shape == (B, lm_model.dep_q, lm_model.card), all_logits.shape
|
| 1175 |
+
return tokens, all_logits
|
| 1176 |
+
else:
|
| 1177 |
+
return tokens
|
| 1178 |
+
|
moshi/models/loaders.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
"""Retrieves the pretrained models for Moshi and Mimi."""
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
import logging
|
| 28 |
+
|
| 29 |
+
from safetensors.torch import load_model, load_file
|
| 30 |
+
import torch
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
from .compression import MimiModel
|
| 35 |
+
from .lm import LMModel
|
| 36 |
+
from ..modules import SEANetEncoder, SEANetDecoder, transformer
|
| 37 |
+
from ..quantization import SplitResidualVectorQuantizer
|
| 38 |
+
|
| 39 |
+
SAMPLE_RATE = 24000
|
| 40 |
+
FRAME_RATE = 12.5
|
| 41 |
+
|
| 42 |
+
TEXT_TOKENIZER_NAME = 'tokenizer_spm_32k_3.model'
|
| 43 |
+
MOSHI_NAME = 'model.safetensors'
|
| 44 |
+
MIMI_NAME = 'tokenizer-e351c8d8-checkpoint125.safetensors'
|
| 45 |
+
DEFAULT_REPO = 'nvidia/personaplex-7b-v1'
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
_seanet_kwargs = {
|
| 49 |
+
"channels": 1,
|
| 50 |
+
"dimension": 512,
|
| 51 |
+
"causal": True,
|
| 52 |
+
"n_filters": 64,
|
| 53 |
+
"n_residual_layers": 1,
|
| 54 |
+
"activation": "ELU",
|
| 55 |
+
"compress": 2,
|
| 56 |
+
"dilation_base": 2,
|
| 57 |
+
"disable_norm_outer_blocks": 0,
|
| 58 |
+
"kernel_size": 7,
|
| 59 |
+
"residual_kernel_size": 3,
|
| 60 |
+
"last_kernel_size": 3,
|
| 61 |
+
# We train using weight_norm but then the weights are pre-processed for inference so
|
| 62 |
+
# that we can use a normal convolution.
|
| 63 |
+
"norm": "none",
|
| 64 |
+
"pad_mode": "constant",
|
| 65 |
+
"ratios": [8, 6, 5, 4],
|
| 66 |
+
"true_skip": True,
|
| 67 |
+
}
|
| 68 |
+
_quantizer_kwargs = {
|
| 69 |
+
"dimension": 256,
|
| 70 |
+
"n_q": 32,
|
| 71 |
+
"bins": 2048,
|
| 72 |
+
"input_dimension": _seanet_kwargs["dimension"],
|
| 73 |
+
"output_dimension": _seanet_kwargs["dimension"],
|
| 74 |
+
}
|
| 75 |
+
_transformer_kwargs = {
|
| 76 |
+
"d_model": _seanet_kwargs["dimension"],
|
| 77 |
+
"num_heads": 8,
|
| 78 |
+
"num_layers": 8,
|
| 79 |
+
"causal": True,
|
| 80 |
+
"layer_scale": 0.01,
|
| 81 |
+
"context": 250,
|
| 82 |
+
"conv_layout": True,
|
| 83 |
+
"max_period": 10000,
|
| 84 |
+
"gating": "none",
|
| 85 |
+
"norm": "layer_norm",
|
| 86 |
+
"positional_embedding": "rope",
|
| 87 |
+
"dim_feedforward": 2048,
|
| 88 |
+
"input_dimension": _seanet_kwargs["dimension"],
|
| 89 |
+
"output_dimensions": [_seanet_kwargs["dimension"]],
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
_lm_kwargs = {
|
| 93 |
+
"dim": 4096,
|
| 94 |
+
"text_card": 32000,
|
| 95 |
+
"existing_text_padding_id": 3,
|
| 96 |
+
"n_q": 16,
|
| 97 |
+
"dep_q": 8,
|
| 98 |
+
"card": _quantizer_kwargs["bins"],
|
| 99 |
+
"num_heads": 32,
|
| 100 |
+
"num_layers": 32,
|
| 101 |
+
"hidden_scale": 4.125,
|
| 102 |
+
"causal": True,
|
| 103 |
+
"layer_scale": None,
|
| 104 |
+
"context": 3000,
|
| 105 |
+
"max_period": 10000,
|
| 106 |
+
"gating": "silu",
|
| 107 |
+
"norm": "rms_norm_f32",
|
| 108 |
+
"positional_embedding": "rope",
|
| 109 |
+
"depformer_dim": 1024,
|
| 110 |
+
"depformer_dim_feedforward": int(4.125 * 1024),
|
| 111 |
+
"depformer_num_heads": 16,
|
| 112 |
+
"depformer_num_layers": 6,
|
| 113 |
+
"depformer_causal": True,
|
| 114 |
+
"depformer_layer_scale": None,
|
| 115 |
+
"depformer_multi_linear": True,
|
| 116 |
+
"depformer_context": 8,
|
| 117 |
+
"depformer_max_period": 10000,
|
| 118 |
+
"depformer_gating": "silu",
|
| 119 |
+
"depformer_pos_emb": "none",
|
| 120 |
+
"depformer_weights_per_step": True,
|
| 121 |
+
"delays": [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _is_safetensors(path: Path | str) -> bool:
|
| 126 |
+
return Path(path).suffix in (".safetensors", ".sft", ".sfts")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def get_mimi(filename: str | Path,
|
| 130 |
+
device: torch.device | str = 'cpu') -> MimiModel:
|
| 131 |
+
"""Return a pretrained Mimi model."""
|
| 132 |
+
encoder = SEANetEncoder(**_seanet_kwargs)
|
| 133 |
+
decoder = SEANetDecoder(**_seanet_kwargs)
|
| 134 |
+
encoder_transformer = transformer.ProjectedTransformer(
|
| 135 |
+
device=device, **_transformer_kwargs
|
| 136 |
+
)
|
| 137 |
+
decoder_transformer = transformer.ProjectedTransformer(
|
| 138 |
+
device=device, **_transformer_kwargs
|
| 139 |
+
)
|
| 140 |
+
quantizer = SplitResidualVectorQuantizer(
|
| 141 |
+
**_quantizer_kwargs,
|
| 142 |
+
)
|
| 143 |
+
model = MimiModel(
|
| 144 |
+
encoder,
|
| 145 |
+
decoder,
|
| 146 |
+
quantizer,
|
| 147 |
+
channels=1,
|
| 148 |
+
sample_rate=SAMPLE_RATE,
|
| 149 |
+
frame_rate=FRAME_RATE,
|
| 150 |
+
encoder_frame_rate=SAMPLE_RATE / encoder.hop_length,
|
| 151 |
+
causal=True,
|
| 152 |
+
resample_method="conv",
|
| 153 |
+
encoder_transformer=encoder_transformer,
|
| 154 |
+
decoder_transformer=decoder_transformer,
|
| 155 |
+
).to(device=device)
|
| 156 |
+
model.eval()
|
| 157 |
+
if _is_safetensors(filename):
|
| 158 |
+
load_model(model, filename)
|
| 159 |
+
else:
|
| 160 |
+
pkg = torch.load(filename, "cpu")
|
| 161 |
+
model.load_state_dict(pkg["model"])
|
| 162 |
+
model.set_num_codebooks(8)
|
| 163 |
+
return model
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def get_moshi_lm(
|
| 167 |
+
filename: str | Path | None,
|
| 168 |
+
copy_missing_weights: bool = True,
|
| 169 |
+
device: torch.device | str = "cpu",
|
| 170 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 171 |
+
delays=None,
|
| 172 |
+
cpu_offload: bool = False,
|
| 173 |
+
) -> LMModel:
|
| 174 |
+
"""Return a pretrained Moshi LM model.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
filename: Path to model weights.
|
| 178 |
+
copy_missing_weights: Whether to copy missing weights from existing layers.
|
| 179 |
+
device: Target device for the model.
|
| 180 |
+
dtype: Data type for model weights.
|
| 181 |
+
delays: Optional custom delays configuration.
|
| 182 |
+
cpu_offload: If True, offload model layers to CPU when GPU memory is
|
| 183 |
+
insufficient. Uses accelerate's device_map="auto".
|
| 184 |
+
"""
|
| 185 |
+
# Copy to avoid mutating a shared/global dict
|
| 186 |
+
lm_kwargs = dict(_lm_kwargs)
|
| 187 |
+
lm_kwargs["dep_q"] = 16
|
| 188 |
+
if delays is not None:
|
| 189 |
+
lm_kwargs["delays"] = delays
|
| 190 |
+
|
| 191 |
+
if cpu_offload and filename is not None:
|
| 192 |
+
return _get_moshi_lm_with_offload(
|
| 193 |
+
filename, copy_missing_weights, device, dtype, lm_kwargs
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Init with meta device to avoid init dummy memory
|
| 197 |
+
init_device = "meta" if filename is not None else device
|
| 198 |
+
model = LMModel(device=init_device, dtype=dtype, **lm_kwargs)
|
| 199 |
+
if filename is None:
|
| 200 |
+
model.to(device=device, dtype=dtype)
|
| 201 |
+
model.eval()
|
| 202 |
+
return model
|
| 203 |
+
|
| 204 |
+
filename = str(filename)
|
| 205 |
+
|
| 206 |
+
# Load state_dict
|
| 207 |
+
if filename.endswith(".safetensors"):
|
| 208 |
+
# safetensors does not support mps directly
|
| 209 |
+
dev = torch.device(device) if isinstance(device, str) else device
|
| 210 |
+
if dev.type == "mps":
|
| 211 |
+
state_dict = load_file(filename, device="cpu")
|
| 212 |
+
else:
|
| 213 |
+
state_dict = load_file(filename, device=dev.type)
|
| 214 |
+
else:
|
| 215 |
+
# torch checkpoint
|
| 216 |
+
with open(filename, "rb") as f:
|
| 217 |
+
state_dict = torch.load(f, map_location="cpu")
|
| 218 |
+
# Patch 1: expand depformer self_attn weights if needed
|
| 219 |
+
model_sd = model.state_dict()
|
| 220 |
+
for name, tensor in list(state_dict.items()):
|
| 221 |
+
if "depformer" in name and "self_attn" in name and name in model_sd:
|
| 222 |
+
if tensor.shape != model_sd[name].shape:
|
| 223 |
+
print("Expanding %s", name)
|
| 224 |
+
missing = (
|
| 225 |
+
tensor
|
| 226 |
+
if copy_missing_weights
|
| 227 |
+
else model_sd[name][tensor.shape[0] :]
|
| 228 |
+
)
|
| 229 |
+
state_dict[name] = torch.concat([tensor, missing], dim=0)
|
| 230 |
+
|
| 231 |
+
# Patch 2: fill missing keys by copying 0..7 -> 8..15 for certain groups
|
| 232 |
+
if copy_missing_weights:
|
| 233 |
+
to_replace = ["gating", "linears", "depformer_in", "depformer_emb"]
|
| 234 |
+
for name in model_sd.keys():
|
| 235 |
+
if name in state_dict:
|
| 236 |
+
continue
|
| 237 |
+
replaced = False
|
| 238 |
+
for old, new in zip(range(8), range(8, 16)):
|
| 239 |
+
for rep in to_replace:
|
| 240 |
+
needle = f"{rep}.{new}."
|
| 241 |
+
if needle in name:
|
| 242 |
+
src = name.replace(needle, f"{rep}.{old}.")
|
| 243 |
+
if src in state_dict:
|
| 244 |
+
print("Replacing %s <- %s", name, src)
|
| 245 |
+
state_dict[name] = state_dict[src]
|
| 246 |
+
replaced = True
|
| 247 |
+
break
|
| 248 |
+
if replaced:
|
| 249 |
+
break
|
| 250 |
+
if not replaced:
|
| 251 |
+
print("Missing %s", name)
|
| 252 |
+
|
| 253 |
+
# Assign weights to target device
|
| 254 |
+
dev = torch.device(device) if isinstance(device, str) else device
|
| 255 |
+
for key in state_dict:
|
| 256 |
+
state_dict[key] = state_dict[key].to(device=dev, dtype=dtype)
|
| 257 |
+
|
| 258 |
+
model.load_state_dict(state_dict, strict=False, assign=True)
|
| 259 |
+
model.eval()
|
| 260 |
+
return model.to(device=device, dtype=dtype)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _get_moshi_lm_with_offload(
|
| 264 |
+
filename: str | Path,
|
| 265 |
+
copy_missing_weights: bool,
|
| 266 |
+
device: torch.device | str,
|
| 267 |
+
dtype: torch.dtype,
|
| 268 |
+
lm_kwargs: dict,
|
| 269 |
+
) -> LMModel:
|
| 270 |
+
"""Load Moshi LM with CPU offloading using accelerate.
|
| 271 |
+
|
| 272 |
+
This function distributes model layers across GPU and CPU based on
|
| 273 |
+
available GPU memory. Layers that don't fit on GPU are kept on CPU
|
| 274 |
+
and moved to GPU only during forward pass.
|
| 275 |
+
"""
|
| 276 |
+
try:
|
| 277 |
+
from accelerate import infer_auto_device_map, dispatch_model
|
| 278 |
+
except ImportError:
|
| 279 |
+
raise ImportError(
|
| 280 |
+
"CPU offloading requires the 'accelerate' package. "
|
| 281 |
+
"Install it with: pip install accelerate"
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
filename = str(filename)
|
| 285 |
+
logger.info("Loading model with CPU offloading enabled")
|
| 286 |
+
|
| 287 |
+
# First, create model on CPU to get the architecture
|
| 288 |
+
model = LMModel(device="cpu", dtype=dtype, **lm_kwargs)
|
| 289 |
+
|
| 290 |
+
# Load state_dict to CPU
|
| 291 |
+
if filename.endswith(".safetensors"):
|
| 292 |
+
state_dict = load_file(filename, device="cpu")
|
| 293 |
+
else:
|
| 294 |
+
with open(filename, "rb") as f:
|
| 295 |
+
state_dict = torch.load(f, map_location="cpu")
|
| 296 |
+
|
| 297 |
+
# Apply weight patches (same as non-offload path)
|
| 298 |
+
model_sd = model.state_dict()
|
| 299 |
+
for name, tensor in list(state_dict.items()):
|
| 300 |
+
if "depformer" in name and "self_attn" in name and name in model_sd:
|
| 301 |
+
if tensor.shape != model_sd[name].shape:
|
| 302 |
+
logger.info(f"Expanding {name}")
|
| 303 |
+
missing = (
|
| 304 |
+
tensor
|
| 305 |
+
if copy_missing_weights
|
| 306 |
+
else model_sd[name][tensor.shape[0]:]
|
| 307 |
+
)
|
| 308 |
+
state_dict[name] = torch.concat([tensor, missing], dim=0)
|
| 309 |
+
|
| 310 |
+
if copy_missing_weights:
|
| 311 |
+
to_replace = ["gating", "linears", "depformer_in", "depformer_emb"]
|
| 312 |
+
for name in model_sd.keys():
|
| 313 |
+
if name in state_dict:
|
| 314 |
+
continue
|
| 315 |
+
replaced = False
|
| 316 |
+
for old, new in zip(range(8), range(8, 16)):
|
| 317 |
+
for rep in to_replace:
|
| 318 |
+
needle = f"{rep}.{new}."
|
| 319 |
+
if needle in name:
|
| 320 |
+
src = name.replace(needle, f"{rep}.{old}.")
|
| 321 |
+
if src in state_dict:
|
| 322 |
+
logger.info(f"Replacing {name} <- {src}")
|
| 323 |
+
state_dict[name] = state_dict[src]
|
| 324 |
+
replaced = True
|
| 325 |
+
break
|
| 326 |
+
if replaced:
|
| 327 |
+
break
|
| 328 |
+
if not replaced:
|
| 329 |
+
logger.warning(f"Missing {name}")
|
| 330 |
+
|
| 331 |
+
model.load_state_dict(state_dict, strict=False, assign=True)
|
| 332 |
+
|
| 333 |
+
# Determine target device
|
| 334 |
+
dev = torch.device(device) if isinstance(device, str) else device
|
| 335 |
+
|
| 336 |
+
if dev.type != "cuda":
|
| 337 |
+
# If not using CUDA, just move to the target device without offloading
|
| 338 |
+
logger.info(f"CPU offload requested but device is {dev}, skipping offload")
|
| 339 |
+
model.to(dev)
|
| 340 |
+
model.eval()
|
| 341 |
+
return model
|
| 342 |
+
|
| 343 |
+
# Infer device map based on available GPU memory
|
| 344 |
+
device_map = infer_auto_device_map(
|
| 345 |
+
model,
|
| 346 |
+
max_memory=None, # Let accelerate auto-detect available memory
|
| 347 |
+
no_split_module_classes=["StreamingTransformerLayer"],
|
| 348 |
+
dtype=dtype,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Log the device distribution
|
| 352 |
+
gpu_layers = sum(1 for v in device_map.values() if v == 0 or v == "cuda:0")
|
| 353 |
+
cpu_layers = sum(1 for v in device_map.values() if v == "cpu")
|
| 354 |
+
logger.info(f"Device map: {gpu_layers} modules on GPU, {cpu_layers} modules on CPU")
|
| 355 |
+
|
| 356 |
+
# Dispatch model across devices
|
| 357 |
+
model = dispatch_model(
|
| 358 |
+
model,
|
| 359 |
+
device_map=device_map,
|
| 360 |
+
offload_dir="offload_weights", # Directory for disk offload if needed
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
model.eval()
|
| 364 |
+
return model
|
moshi/modules/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
"""Modules used for building the models."""
|
| 11 |
+
|
| 12 |
+
# flake8: noqa
|
| 13 |
+
from .conv import (
|
| 14 |
+
NormConv1d,
|
| 15 |
+
NormConvTranspose1d,
|
| 16 |
+
StreamingConv1d,
|
| 17 |
+
StreamingConvTranspose1d,
|
| 18 |
+
pad_for_conv1d,
|
| 19 |
+
pad1d,
|
| 20 |
+
unpad1d,
|
| 21 |
+
)
|
| 22 |
+
from .seanet import SEANetEncoder, SEANetDecoder
|
| 23 |
+
from .transformer import StreamingTransformer
|
moshi/modules/conv.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
from dataclasses import dataclass
|
| 33 |
+
import math
|
| 34 |
+
import typing as tp
|
| 35 |
+
import warnings
|
| 36 |
+
|
| 37 |
+
import torch
|
| 38 |
+
from torch import nn
|
| 39 |
+
from torch.nn import functional as F
|
| 40 |
+
from torch.nn.utils import weight_norm
|
| 41 |
+
|
| 42 |
+
from .streaming import RawStreamingConv1d, RawStreamingConvTranspose1d, StreamingModule
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
CONV_NORMALIZATIONS = frozenset(["none", "weight_norm"])
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TransposedLayerNorm(nn.Module):
|
| 49 |
+
"""LayerNorm for [B, C, T] inputs."""
|
| 50 |
+
|
| 51 |
+
def __init__(self, **kwargs):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.layer_norm = nn.LayerNorm(**kwargs)
|
| 54 |
+
|
| 55 |
+
def forward(self, x):
|
| 56 |
+
x = x.transpose(1, 2)
|
| 57 |
+
x = self.layer_norm(x)
|
| 58 |
+
return x.transpose(1, 2)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def apply_parametrization_norm(module: nn.Module, norm: str = "none"):
|
| 62 |
+
assert norm in CONV_NORMALIZATIONS
|
| 63 |
+
if norm == "weight_norm":
|
| 64 |
+
return weight_norm(module)
|
| 65 |
+
else:
|
| 66 |
+
# We already check was in CONV_NORMALIZATION, so any other choice
|
| 67 |
+
# doesn't need reparametrization.
|
| 68 |
+
return module
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def get_extra_padding_for_conv1d(
|
| 72 |
+
x: torch.Tensor, kernel_size: int, stride: int, padding_total: int = 0
|
| 73 |
+
) -> int:
|
| 74 |
+
"""See `pad_for_conv1d`."""
|
| 75 |
+
length = x.shape[-1]
|
| 76 |
+
n_frames = (length - kernel_size + padding_total) / stride + 1
|
| 77 |
+
ideal_length = (math.ceil(n_frames) - 1) * stride + (kernel_size - padding_total)
|
| 78 |
+
return ideal_length - length
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def pad_for_conv1d(
|
| 82 |
+
x: torch.Tensor, kernel_size: int, stride: int, padding_total: int = 0
|
| 83 |
+
):
|
| 84 |
+
"""Pad for a convolution to make sure that the last window is full.
|
| 85 |
+
Extra padding is added at the end. This is required to ensure that we can rebuild
|
| 86 |
+
an output of the same length, as otherwise, even with padding, some time steps
|
| 87 |
+
might get removed.
|
| 88 |
+
For instance, with total padding = 4, kernel size = 4, stride = 2:
|
| 89 |
+
0 0 1 2 3 4 5 0 0 # (0s are padding)
|
| 90 |
+
1 2 3 # (output frames of a convolution, last 0 is never used)
|
| 91 |
+
0 0 1 2 3 4 5 0 # (output of tr. conv., but pos. 5 is going to get removed as padding)
|
| 92 |
+
1 2 3 4 # once you removed padding, we are missing one time step !
|
| 93 |
+
"""
|
| 94 |
+
extra_padding = get_extra_padding_for_conv1d(x, kernel_size, stride, padding_total)
|
| 95 |
+
return F.pad(x, (0, extra_padding))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def pad1d(
|
| 99 |
+
x: torch.Tensor,
|
| 100 |
+
paddings: tp.Tuple[int, int],
|
| 101 |
+
mode: str = "constant",
|
| 102 |
+
value: float = 0.0,
|
| 103 |
+
):
|
| 104 |
+
"""Tiny wrapper around F.pad, just to allow for reflect padding on small input.
|
| 105 |
+
If this is the case, we insert extra 0 padding to the right before the reflection happen.
|
| 106 |
+
"""
|
| 107 |
+
length = x.shape[-1]
|
| 108 |
+
padding_left, padding_right = paddings
|
| 109 |
+
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
| 110 |
+
if mode == "reflect":
|
| 111 |
+
max_pad = max(padding_left, padding_right)
|
| 112 |
+
extra_pad = 0
|
| 113 |
+
if length <= max_pad:
|
| 114 |
+
extra_pad = max_pad - length + 1
|
| 115 |
+
x = F.pad(x, (0, extra_pad))
|
| 116 |
+
padded = F.pad(x, paddings, mode, value)
|
| 117 |
+
end = padded.shape[-1] - extra_pad
|
| 118 |
+
return padded[..., :end]
|
| 119 |
+
else:
|
| 120 |
+
return F.pad(x, paddings, mode, value)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def unpad1d(x: torch.Tensor, paddings: tp.Tuple[int, int]):
|
| 124 |
+
"""Remove padding from x, handling properly zero padding. Only for 1d!"""
|
| 125 |
+
padding_left, padding_right = paddings
|
| 126 |
+
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
| 127 |
+
assert (padding_left + padding_right) <= x.shape[-1]
|
| 128 |
+
end = x.shape[-1] - padding_right
|
| 129 |
+
return x[..., padding_left:end]
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class NormConv1d(nn.Module):
|
| 133 |
+
"""Wrapper around Conv1d and normalization applied to this conv
|
| 134 |
+
to provide a uniform interface across normalization approaches.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(
|
| 138 |
+
self,
|
| 139 |
+
*args,
|
| 140 |
+
causal: bool = False,
|
| 141 |
+
norm: str = "none",
|
| 142 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {},
|
| 143 |
+
**kwargs,
|
| 144 |
+
):
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.conv = apply_parametrization_norm(
|
| 147 |
+
RawStreamingConv1d(*args, **kwargs), norm
|
| 148 |
+
)
|
| 149 |
+
self.norm_type = norm
|
| 150 |
+
|
| 151 |
+
def forward(self, x):
|
| 152 |
+
x = self.conv(x)
|
| 153 |
+
return x
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class NormConvTranspose1d(nn.Module):
|
| 157 |
+
"""Wrapper around ConvTranspose1d and normalization applied to this conv
|
| 158 |
+
to provide a uniform interface across normalization approaches.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
def __init__(
|
| 162 |
+
self,
|
| 163 |
+
*args,
|
| 164 |
+
causal: bool = False,
|
| 165 |
+
norm: str = "none",
|
| 166 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {},
|
| 167 |
+
**kwargs,
|
| 168 |
+
):
|
| 169 |
+
super().__init__()
|
| 170 |
+
self.convtr = apply_parametrization_norm(
|
| 171 |
+
RawStreamingConvTranspose1d(*args, **kwargs), norm
|
| 172 |
+
)
|
| 173 |
+
self.norm_type = norm
|
| 174 |
+
|
| 175 |
+
def forward(self, x):
|
| 176 |
+
x = self.convtr(x)
|
| 177 |
+
return x
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@dataclass
|
| 181 |
+
class _StreamingConv1dState:
|
| 182 |
+
padding_to_add: int
|
| 183 |
+
original_padding_to_add: int
|
| 184 |
+
|
| 185 |
+
def reset(self):
|
| 186 |
+
self.padding_to_add = self.original_padding_to_add
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class StreamingConv1d(StreamingModule[_StreamingConv1dState]):
|
| 190 |
+
"""Conv1d with some builtin handling of asymmetric or causal padding
|
| 191 |
+
and normalization.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
def __init__(
|
| 195 |
+
self,
|
| 196 |
+
in_channels: int,
|
| 197 |
+
out_channels: int,
|
| 198 |
+
kernel_size: int,
|
| 199 |
+
stride: int = 1,
|
| 200 |
+
dilation: int = 1,
|
| 201 |
+
groups: int = 1,
|
| 202 |
+
bias: bool = True,
|
| 203 |
+
causal: bool = False,
|
| 204 |
+
norm: str = "none",
|
| 205 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {},
|
| 206 |
+
pad_mode: str = "reflect",
|
| 207 |
+
):
|
| 208 |
+
super().__init__()
|
| 209 |
+
# warn user on unusual setup between dilation and stride
|
| 210 |
+
if stride > 1 and dilation > 1:
|
| 211 |
+
warnings.warn(
|
| 212 |
+
"StreamingConv1d has been initialized with stride > 1 and dilation > 1"
|
| 213 |
+
f" (kernel_size={kernel_size} stride={stride}, dilation={dilation})."
|
| 214 |
+
)
|
| 215 |
+
self.conv = NormConv1d(
|
| 216 |
+
in_channels,
|
| 217 |
+
out_channels,
|
| 218 |
+
kernel_size,
|
| 219 |
+
stride,
|
| 220 |
+
dilation=dilation,
|
| 221 |
+
groups=groups,
|
| 222 |
+
bias=bias,
|
| 223 |
+
causal=causal,
|
| 224 |
+
norm=norm,
|
| 225 |
+
norm_kwargs=norm_kwargs,
|
| 226 |
+
)
|
| 227 |
+
self.causal = causal
|
| 228 |
+
self.pad_mode = pad_mode
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def _stride(self) -> int:
|
| 232 |
+
return self.conv.conv.stride[0]
|
| 233 |
+
|
| 234 |
+
@property
|
| 235 |
+
def _kernel_size(self) -> int:
|
| 236 |
+
return self.conv.conv.kernel_size[0]
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def _effective_kernel_size(self) -> int:
|
| 240 |
+
dilation = self.conv.conv.dilation[0]
|
| 241 |
+
return (
|
| 242 |
+
self._kernel_size - 1
|
| 243 |
+
) * dilation + 1 # effective kernel size with dilations
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def _padding_total(self) -> int:
|
| 247 |
+
return self._effective_kernel_size - self._stride
|
| 248 |
+
|
| 249 |
+
def _init_streaming_state(self, batch_size: int) -> _StreamingConv1dState:
|
| 250 |
+
assert self.causal, "streaming is only supported for causal convs"
|
| 251 |
+
return _StreamingConv1dState(self._padding_total, self._padding_total)
|
| 252 |
+
|
| 253 |
+
def forward(self, x):
|
| 254 |
+
B, C, T = x.shape
|
| 255 |
+
padding_total = self._padding_total
|
| 256 |
+
extra_padding = get_extra_padding_for_conv1d(
|
| 257 |
+
x, self._effective_kernel_size, self._stride, padding_total
|
| 258 |
+
)
|
| 259 |
+
state = self._streaming_state
|
| 260 |
+
if state is None:
|
| 261 |
+
if self.causal:
|
| 262 |
+
# Left padding for causal
|
| 263 |
+
x = pad1d(x, (padding_total, extra_padding), mode=self.pad_mode)
|
| 264 |
+
else:
|
| 265 |
+
# Asymmetric padding required for odd strides
|
| 266 |
+
padding_right = padding_total // 2
|
| 267 |
+
padding_left = padding_total - padding_right
|
| 268 |
+
x = pad1d(
|
| 269 |
+
x, (padding_left, padding_right + extra_padding), mode=self.pad_mode
|
| 270 |
+
)
|
| 271 |
+
else:
|
| 272 |
+
if state.padding_to_add > 0 and x.shape[-1] > 0:
|
| 273 |
+
x = pad1d(x, (state.padding_to_add, 0), mode=self.pad_mode)
|
| 274 |
+
state.padding_to_add = 0
|
| 275 |
+
return self.conv(x)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@dataclass
|
| 279 |
+
class _StreamingConvTr1dState:
|
| 280 |
+
pass
|
| 281 |
+
|
| 282 |
+
def reset(self):
|
| 283 |
+
pass
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class StreamingConvTranspose1d(StreamingModule[_StreamingConvTr1dState]):
|
| 287 |
+
"""ConvTranspose1d with some builtin handling of asymmetric or causal padding
|
| 288 |
+
and normalization.
|
| 289 |
+
"""
|
| 290 |
+
|
| 291 |
+
def __init__(
|
| 292 |
+
self,
|
| 293 |
+
in_channels: int,
|
| 294 |
+
out_channels: int,
|
| 295 |
+
kernel_size: int,
|
| 296 |
+
stride: int = 1,
|
| 297 |
+
groups: int = 1,
|
| 298 |
+
bias: bool = True,
|
| 299 |
+
causal: bool = False,
|
| 300 |
+
norm: str = "none",
|
| 301 |
+
trim_right_ratio: float = 1.0,
|
| 302 |
+
norm_kwargs: tp.Dict[str, tp.Any] = {},
|
| 303 |
+
):
|
| 304 |
+
super().__init__()
|
| 305 |
+
self.convtr = NormConvTranspose1d(
|
| 306 |
+
in_channels,
|
| 307 |
+
out_channels,
|
| 308 |
+
kernel_size,
|
| 309 |
+
stride,
|
| 310 |
+
groups=groups,
|
| 311 |
+
bias=bias,
|
| 312 |
+
causal=causal,
|
| 313 |
+
norm=norm,
|
| 314 |
+
norm_kwargs=norm_kwargs,
|
| 315 |
+
)
|
| 316 |
+
self.causal = causal
|
| 317 |
+
self.trim_right_ratio = trim_right_ratio
|
| 318 |
+
assert (
|
| 319 |
+
self.causal or self.trim_right_ratio == 1.0
|
| 320 |
+
), "`trim_right_ratio` != 1.0 only makes sense for causal convolutions"
|
| 321 |
+
assert self.trim_right_ratio >= 0.0 and self.trim_right_ratio <= 1.0
|
| 322 |
+
|
| 323 |
+
def _init_streaming_state(self, batch_size: int) -> _StreamingConvTr1dState:
|
| 324 |
+
assert self.causal, "streaming is only supported for causal convtrs"
|
| 325 |
+
return _StreamingConvTr1dState()
|
| 326 |
+
|
| 327 |
+
def forward(self, x):
|
| 328 |
+
kernel_size = self.convtr.convtr.kernel_size[0]
|
| 329 |
+
stride = self.convtr.convtr.stride[0]
|
| 330 |
+
padding_total = kernel_size - stride
|
| 331 |
+
|
| 332 |
+
y = self.convtr(x)
|
| 333 |
+
|
| 334 |
+
if not self.is_streaming:
|
| 335 |
+
# We will only trim fixed padding. Extra padding from `pad_for_conv1d` would be
|
| 336 |
+
# removed at the very end, when keeping only the right length for the output,
|
| 337 |
+
# as removing it here would require also passing the length at the matching layer
|
| 338 |
+
# in the encoder.
|
| 339 |
+
if self.causal:
|
| 340 |
+
# Trim the padding on the right according to the specified ratio
|
| 341 |
+
# if trim_right_ratio = 1.0, trim everything from right
|
| 342 |
+
padding_right = math.ceil(padding_total * self.trim_right_ratio)
|
| 343 |
+
padding_left = padding_total - padding_right
|
| 344 |
+
y = unpad1d(y, (padding_left, padding_right))
|
| 345 |
+
else:
|
| 346 |
+
# Asymmetric padding required for odd strides
|
| 347 |
+
padding_right = padding_total // 2
|
| 348 |
+
padding_left = padding_total - padding_right
|
| 349 |
+
y = unpad1d(y, (padding_left, padding_right))
|
| 350 |
+
return y
|
moshi/modules/gating.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from torch import nn
|
| 28 |
+
from torch.nn import functional as F
|
| 29 |
+
|
| 30 |
+
from ..utils.compile import torch_compile_lazy
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@torch_compile_lazy
|
| 34 |
+
def gating_forward_kernel(
|
| 35 |
+
weight_in: torch.Tensor, weight_out: torch.Tensor, activation, x: torch.Tensor
|
| 36 |
+
):
|
| 37 |
+
x = F.linear(x, weight_in)
|
| 38 |
+
B, T, _ = x.shape
|
| 39 |
+
x = x.view(B, T, 2, -1)
|
| 40 |
+
x = activation(x[..., 0, :]) * x[..., 1, :]
|
| 41 |
+
x = F.linear(x, weight_out)
|
| 42 |
+
return x
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class ActivationGating(nn.Module):
|
| 46 |
+
"""
|
| 47 |
+
Gating FFN layer, using the given activation.
|
| 48 |
+
Args:
|
| 49 |
+
dim (int): dimension of the input and output of the transformer.
|
| 50 |
+
activation (any callable Tensor to Tensor): activation function to use.
|
| 51 |
+
**factory_kwargs: other kwargs passed to the linear layer, in particular device and dtype.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
_fsdp_final = True
|
| 55 |
+
|
| 56 |
+
def __init__(self, dim: int, dim_feedforward: int, activation, **factory_kwargs):
|
| 57 |
+
super().__init__()
|
| 58 |
+
# We should have 8 d^2 param, instead we will have
|
| 59 |
+
# 2 * h * d + h * d = 3 h * d = 8 d^2
|
| 60 |
+
# so h = 8 d / 3 but following Hervé's advice we use 21 / 8 as an approx.
|
| 61 |
+
if dim_feedforward == 4 * dim:
|
| 62 |
+
hidden = (21 * dim) // 8
|
| 63 |
+
else:
|
| 64 |
+
hidden = (2 * dim_feedforward) // 3
|
| 65 |
+
self.linear_in = nn.Linear(dim, 2 * hidden, bias=False, **factory_kwargs)
|
| 66 |
+
self.linear_out = nn.Linear(hidden, dim, bias=False, **factory_kwargs)
|
| 67 |
+
self.activation = activation
|
| 68 |
+
|
| 69 |
+
def forward(self, x: torch.Tensor):
|
| 70 |
+
return gating_forward_kernel(
|
| 71 |
+
self.linear_in.weight, self.linear_out.weight, self.activation, x
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _get_activation(name: str):
|
| 76 |
+
if name in ["sigmoid", "tanh", "relu"]:
|
| 77 |
+
return getattr(torch, name)
|
| 78 |
+
elif name in ["leaky_relu", "elu", "gelu", "silu", "mish", "softsign"]:
|
| 79 |
+
return getattr(torch.nn.functional, name)
|
| 80 |
+
elif name == "identity":
|
| 81 |
+
return torch.nn.Identity()
|
| 82 |
+
else:
|
| 83 |
+
raise ValueError(f"Unknown activation {name}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _make_gating(
|
| 87 |
+
name: str, dim: int, dim_feedforward: int, **factory_kwargs
|
| 88 |
+
) -> nn.Module:
|
| 89 |
+
return ActivationGating(
|
| 90 |
+
dim, dim_feedforward, _get_activation(name), **factory_kwargs
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def make_gating(
|
| 95 |
+
name: str, dim: int, dim_feedforward: int, **factory_kwargs
|
| 96 |
+
) -> nn.Module:
|
| 97 |
+
gating = _make_gating(name, dim, dim_feedforward, **factory_kwargs)
|
| 98 |
+
max_params = 2 * dim * dim_feedforward
|
| 99 |
+
params = sum(p.numel() for p in gating.parameters())
|
| 100 |
+
assert (
|
| 101 |
+
params <= max_params
|
| 102 |
+
), f"{name} gating has {params} params, max is {max_params}"
|
| 103 |
+
return gating
|
moshi/modules/resample.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
import typing as tp
|
| 27 |
+
|
| 28 |
+
from einops import rearrange
|
| 29 |
+
import torch
|
| 30 |
+
from torch import nn
|
| 31 |
+
|
| 32 |
+
from .conv import StreamingConv1d, StreamingConvTranspose1d
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ConvDownsample1d(nn.Module):
|
| 36 |
+
"""
|
| 37 |
+
Downsampling by some integer amount `stride` using convolutions
|
| 38 |
+
with a kernel size of twice the stride.
|
| 39 |
+
If `causal` is True, the output uses a causal convolution.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
stride: int,
|
| 45 |
+
dimension: tp.Optional[int] = None,
|
| 46 |
+
causal: bool = False,
|
| 47 |
+
learnt: bool = False,
|
| 48 |
+
channel_wise: bool = False,
|
| 49 |
+
):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.learnt = learnt
|
| 52 |
+
self.channel_wise = channel_wise
|
| 53 |
+
groups = 1
|
| 54 |
+
if learnt:
|
| 55 |
+
assert dimension is not None, "Dimension required for learnt convolutions."
|
| 56 |
+
in_channels = dimension
|
| 57 |
+
out_channels = dimension
|
| 58 |
+
if channel_wise:
|
| 59 |
+
groups = dimension
|
| 60 |
+
else:
|
| 61 |
+
in_channels = 1
|
| 62 |
+
out_channels = 1
|
| 63 |
+
|
| 64 |
+
self.conv = StreamingConv1d(
|
| 65 |
+
in_channels,
|
| 66 |
+
out_channels,
|
| 67 |
+
kernel_size=2 * stride,
|
| 68 |
+
stride=stride,
|
| 69 |
+
causal=causal,
|
| 70 |
+
groups=groups,
|
| 71 |
+
bias=False,
|
| 72 |
+
pad_mode="replicate",
|
| 73 |
+
)
|
| 74 |
+
if not learnt:
|
| 75 |
+
actual_conv = self.conv.conv.conv
|
| 76 |
+
actual_conv.weight.requires_grad_(False)
|
| 77 |
+
actual_conv.weight.data.fill_(1.0 / (2 * stride))
|
| 78 |
+
|
| 79 |
+
def forward(self, x: torch.Tensor):
|
| 80 |
+
batch_size = len(x)
|
| 81 |
+
if not self.learnt:
|
| 82 |
+
x = rearrange(x, "b c t -> (b c) () t")
|
| 83 |
+
y = self.conv(x)
|
| 84 |
+
if not self.learnt:
|
| 85 |
+
y = rearrange(y, "(b c) () t -> b c t", b=batch_size)
|
| 86 |
+
return y
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class ConvTrUpsample1d(nn.Module):
|
| 90 |
+
"""
|
| 91 |
+
Upsample by some integer amount `stride` using transposed convolutions.
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(
|
| 95 |
+
self,
|
| 96 |
+
stride: int,
|
| 97 |
+
dimension: tp.Optional[int] = None,
|
| 98 |
+
causal: bool = False,
|
| 99 |
+
learnt: bool = False,
|
| 100 |
+
channel_wise: bool = False,
|
| 101 |
+
):
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.learnt = learnt
|
| 104 |
+
self.channel_wise = channel_wise
|
| 105 |
+
groups = 1
|
| 106 |
+
if learnt:
|
| 107 |
+
assert dimension is not None, "Dimension required for learnt convolutions."
|
| 108 |
+
in_channels = dimension
|
| 109 |
+
out_channels = dimension
|
| 110 |
+
if channel_wise:
|
| 111 |
+
groups = dimension
|
| 112 |
+
else:
|
| 113 |
+
in_channels = 1
|
| 114 |
+
out_channels = 1
|
| 115 |
+
|
| 116 |
+
self.convtr = StreamingConvTranspose1d(
|
| 117 |
+
in_channels,
|
| 118 |
+
out_channels,
|
| 119 |
+
kernel_size=2 * stride,
|
| 120 |
+
stride=stride,
|
| 121 |
+
causal=causal,
|
| 122 |
+
groups=groups,
|
| 123 |
+
bias=False,
|
| 124 |
+
)
|
| 125 |
+
if not learnt:
|
| 126 |
+
actual_convtr = self.convtr.convtr.convtr
|
| 127 |
+
actual_convtr.weight.requires_grad_(False)
|
| 128 |
+
actual_convtr.weight.data.fill_(1.0)
|
| 129 |
+
|
| 130 |
+
def forward(self, x: torch.Tensor):
|
| 131 |
+
batch_size = len(x)
|
| 132 |
+
if not self.learnt:
|
| 133 |
+
x = rearrange(x, "b c t -> (b c) () t")
|
| 134 |
+
y = self.convtr(x)
|
| 135 |
+
if not self.learnt:
|
| 136 |
+
x_for_normalization = torch.ones_like(x[:1])
|
| 137 |
+
normalization = self.convtr(x_for_normalization)
|
| 138 |
+
y = y / normalization
|
| 139 |
+
y = rearrange(y, "(b c) () t -> b c t", b=batch_size)
|
| 140 |
+
return y
|
moshi/modules/rope.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
from torch import nn
|
| 27 |
+
import math
|
| 28 |
+
import torch
|
| 29 |
+
from ..utils.compile import torch_compile_lazy
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@torch_compile_lazy
|
| 33 |
+
def apply_rope(
|
| 34 |
+
q: torch.Tensor,
|
| 35 |
+
k: torch.Tensor,
|
| 36 |
+
offset: torch.Tensor,
|
| 37 |
+
max_period: float = 10_000,
|
| 38 |
+
time_before_heads: bool = False,
|
| 39 |
+
):
|
| 40 |
+
"""
|
| 41 |
+
Args:
|
| 42 |
+
q (torch.Tensor): queries, shape `[B, T, H, D]`.
|
| 43 |
+
k (torch.Tensor): keys, shape `[B, T, H, D]`.
|
| 44 |
+
offset (int): current offset, e.g. when streaming.
|
| 45 |
+
max_period (float): maximum period for the cos and sin.
|
| 46 |
+
time_before_heads (bool): if True, expected [B, T, H, D], else [B, H, T ,D]
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
if time_before_heads:
|
| 50 |
+
B, T, H, D = q.shape
|
| 51 |
+
else:
|
| 52 |
+
B, H, T, D = q.shape
|
| 53 |
+
assert k.shape == q.shape
|
| 54 |
+
assert D > 0
|
| 55 |
+
assert D % 2 == 0
|
| 56 |
+
assert max_period > 0
|
| 57 |
+
|
| 58 |
+
ds = torch.arange(D // 2, device=q.device, dtype=torch.float32)
|
| 59 |
+
freqs = torch.exp(ds * (-math.log(max_period) * 2 / D))
|
| 60 |
+
ts = offset.float() + torch.arange(T, device=q.device, dtype=torch.float32)
|
| 61 |
+
if time_before_heads:
|
| 62 |
+
ts = ts.view(-1, 1, 1)
|
| 63 |
+
else:
|
| 64 |
+
ts = ts.view(1, -1, 1)
|
| 65 |
+
|
| 66 |
+
dims = q.shape[:-1]
|
| 67 |
+
q = q.view(*dims, D // 2, 2)
|
| 68 |
+
k = k.view(*dims, D // 2, 2)
|
| 69 |
+
|
| 70 |
+
# convention is `r` suffix is real part, `i` is imaginary.
|
| 71 |
+
qr = q[..., 0].float()
|
| 72 |
+
qi = q[..., 1].float()
|
| 73 |
+
|
| 74 |
+
kr = k[..., 0].float()
|
| 75 |
+
ki = k[..., 1].float()
|
| 76 |
+
|
| 77 |
+
rotr = torch.cos(freqs * ts)
|
| 78 |
+
roti = torch.sin(freqs * ts)
|
| 79 |
+
qor = qr * rotr - qi * roti
|
| 80 |
+
qoi = qr * roti + qi * rotr
|
| 81 |
+
|
| 82 |
+
kor = kr * rotr - ki * roti
|
| 83 |
+
koi = kr * roti + ki * rotr
|
| 84 |
+
|
| 85 |
+
dtype = q.dtype
|
| 86 |
+
qo = torch.stack([qor.to(dtype), qoi.to(dtype)], dim=-1)
|
| 87 |
+
ko = torch.stack([kor.to(dtype), koi.to(dtype)], dim=-1)
|
| 88 |
+
|
| 89 |
+
return qo.view(*dims, D), ko.view(*dims, D)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class RotaryEmbedding(nn.Module):
|
| 93 |
+
"""Rotary positional embedding (RoPE) from [Su et al 2022](https://arxiv.org/abs/2104.09864).
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
max_period (float): Maximum period of the rotation frequencies.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __init__(self, max_period: float = 10000.0):
|
| 100 |
+
super().__init__()
|
| 101 |
+
self.max_period = max_period
|
| 102 |
+
|
| 103 |
+
def forward(
|
| 104 |
+
self,
|
| 105 |
+
q: torch.Tensor,
|
| 106 |
+
k: torch.Tensor,
|
| 107 |
+
offset: torch.Tensor,
|
| 108 |
+
time_before_heads: bool = False,
|
| 109 |
+
):
|
| 110 |
+
"""Apply rope rotation to query or key tensor."""
|
| 111 |
+
return apply_rope(q, k, offset, self.max_period, time_before_heads)
|
moshi/modules/seanet.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
import typing as tp
|
| 33 |
+
|
| 34 |
+
import numpy as np
|
| 35 |
+
import torch.nn as nn
|
| 36 |
+
|
| 37 |
+
from .conv import StreamingConv1d, StreamingConvTranspose1d
|
| 38 |
+
from .streaming import StreamingContainer, StreamingAdd
|
| 39 |
+
from ..utils.compile import torch_compile_lazy
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class SEANetResnetBlock(StreamingContainer):
|
| 43 |
+
"""Residual block from SEANet model.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
dim (int): Dimension of the input/output.
|
| 47 |
+
kernel_sizes (list): List of kernel sizes for the convolutions.
|
| 48 |
+
dilations (list): List of dilations for the convolutions.
|
| 49 |
+
activation (str): Activation function.
|
| 50 |
+
activation_params (dict): Parameters to provide to the activation function.
|
| 51 |
+
norm (str): Normalization method.
|
| 52 |
+
norm_params (dict): Parameters to provide to the underlying normalization used along with the convolution.
|
| 53 |
+
causal (bool): Whether to use fully causal convolution.
|
| 54 |
+
pad_mode (str): Padding mode for the convolutions.
|
| 55 |
+
compress (int): Reduced dimensionality in residual branches (from Demucs v3).
|
| 56 |
+
true_skip (bool): Whether to use true skip connection or a simple
|
| 57 |
+
(streamable) convolution as the skip connection.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self,
|
| 62 |
+
dim: int,
|
| 63 |
+
kernel_sizes: tp.List[int] = [3, 1],
|
| 64 |
+
dilations: tp.List[int] = [1, 1],
|
| 65 |
+
activation: str = "ELU",
|
| 66 |
+
activation_params: dict = {"alpha": 1.0},
|
| 67 |
+
norm: str = "none",
|
| 68 |
+
norm_params: tp.Dict[str, tp.Any] = {},
|
| 69 |
+
causal: bool = False,
|
| 70 |
+
pad_mode: str = "reflect",
|
| 71 |
+
compress: int = 2,
|
| 72 |
+
true_skip: bool = True,
|
| 73 |
+
):
|
| 74 |
+
super().__init__()
|
| 75 |
+
assert len(kernel_sizes) == len(
|
| 76 |
+
dilations
|
| 77 |
+
), "Number of kernel sizes should match number of dilations"
|
| 78 |
+
act = getattr(nn, activation)
|
| 79 |
+
hidden = dim // compress
|
| 80 |
+
block = []
|
| 81 |
+
for i, (kernel_size, dilation) in enumerate(zip(kernel_sizes, dilations)):
|
| 82 |
+
in_chs = dim if i == 0 else hidden
|
| 83 |
+
out_chs = dim if i == len(kernel_sizes) - 1 else hidden
|
| 84 |
+
block += [
|
| 85 |
+
act(**activation_params),
|
| 86 |
+
StreamingConv1d(
|
| 87 |
+
in_chs,
|
| 88 |
+
out_chs,
|
| 89 |
+
kernel_size=kernel_size,
|
| 90 |
+
dilation=dilation,
|
| 91 |
+
norm=norm,
|
| 92 |
+
norm_kwargs=norm_params,
|
| 93 |
+
causal=causal,
|
| 94 |
+
pad_mode=pad_mode,
|
| 95 |
+
),
|
| 96 |
+
]
|
| 97 |
+
self.block = nn.Sequential(*block)
|
| 98 |
+
self.add = StreamingAdd()
|
| 99 |
+
self.shortcut: nn.Module
|
| 100 |
+
if true_skip:
|
| 101 |
+
self.shortcut = nn.Identity()
|
| 102 |
+
else:
|
| 103 |
+
self.shortcut = StreamingConv1d(
|
| 104 |
+
dim,
|
| 105 |
+
dim,
|
| 106 |
+
kernel_size=1,
|
| 107 |
+
norm=norm,
|
| 108 |
+
norm_kwargs=norm_params,
|
| 109 |
+
causal=causal,
|
| 110 |
+
pad_mode=pad_mode,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
def forward(self, x):
|
| 114 |
+
u, v = self.shortcut(x), self.block(x)
|
| 115 |
+
return self.add(u, v)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class SEANetEncoder(StreamingContainer):
|
| 119 |
+
"""SEANet encoder.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
channels (int): Audio channels.
|
| 123 |
+
dimension (int): Intermediate representation dimension.
|
| 124 |
+
n_filters (int): Base width for the model.
|
| 125 |
+
n_residual_layers (int): nb of residual layers.
|
| 126 |
+
ratios (Sequence[int]): kernel size and stride ratios. The encoder uses downsampling ratios instead of
|
| 127 |
+
upsampling ratios, hence it will use the ratios in the reverse order to the ones specified here
|
| 128 |
+
that must match the decoder order. We use the decoder order as some models may only employ the decoder.
|
| 129 |
+
activation (str): Activation function.
|
| 130 |
+
activation_params (dict): Parameters to provide to the activation function.
|
| 131 |
+
norm (str): Normalization method.
|
| 132 |
+
norm_params (dict): Parameters to provide to the underlying normalization used along with the convolution.
|
| 133 |
+
kernel_size (int): Kernel size for the initial convolution.
|
| 134 |
+
last_kernel_size (int): Kernel size for the initial convolution.
|
| 135 |
+
residual_kernel_size (int): Kernel size for the residual layers.
|
| 136 |
+
dilation_base (int): How much to increase the dilation with each layer.
|
| 137 |
+
causal (bool): Whether to use fully causal convolution.
|
| 138 |
+
pad_mode (str): Padding mode for the convolutions.
|
| 139 |
+
true_skip (bool): Whether to use true skip connection or a simple
|
| 140 |
+
(streamable) convolution as the skip connection in the residual network blocks.
|
| 141 |
+
compress (int): Reduced dimensionality in residual branches (from Demucs v3).
|
| 142 |
+
disable_norm_outer_blocks (int): Number of blocks for which we don't apply norm.
|
| 143 |
+
For the encoder, it corresponds to the N first blocks.
|
| 144 |
+
mask_fn (nn.Module): Optional mask function to apply after convolution layers.
|
| 145 |
+
mask_position (int): Position of the mask function, with mask_position == 0 for the first convolution layer,
|
| 146 |
+
mask_position == 1 for the first conv block, etc.
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
def __init__(
|
| 150 |
+
self,
|
| 151 |
+
channels: int = 1,
|
| 152 |
+
dimension: int = 128,
|
| 153 |
+
n_filters: int = 32,
|
| 154 |
+
n_residual_layers: int = 3,
|
| 155 |
+
ratios: tp.List[int] = [8, 5, 4, 2],
|
| 156 |
+
activation: str = "ELU",
|
| 157 |
+
activation_params: dict = {"alpha": 1.0},
|
| 158 |
+
norm: str = "none",
|
| 159 |
+
norm_params: tp.Dict[str, tp.Any] = {},
|
| 160 |
+
kernel_size: int = 7,
|
| 161 |
+
last_kernel_size: int = 7,
|
| 162 |
+
residual_kernel_size: int = 3,
|
| 163 |
+
dilation_base: int = 2,
|
| 164 |
+
causal: bool = False,
|
| 165 |
+
pad_mode: str = "reflect",
|
| 166 |
+
true_skip: bool = True,
|
| 167 |
+
compress: int = 2,
|
| 168 |
+
disable_norm_outer_blocks: int = 0,
|
| 169 |
+
mask_fn: tp.Optional[nn.Module] = None,
|
| 170 |
+
mask_position: tp.Optional[int] = None,
|
| 171 |
+
):
|
| 172 |
+
super().__init__()
|
| 173 |
+
self.channels = channels
|
| 174 |
+
self.dimension = dimension
|
| 175 |
+
self.n_filters = n_filters
|
| 176 |
+
self.ratios = list(reversed(ratios))
|
| 177 |
+
del ratios
|
| 178 |
+
self.n_residual_layers = n_residual_layers
|
| 179 |
+
self.hop_length = int(np.prod(self.ratios))
|
| 180 |
+
self.n_blocks = len(self.ratios) + 2 # first and last conv + residual blocks
|
| 181 |
+
self.disable_norm_outer_blocks = disable_norm_outer_blocks
|
| 182 |
+
assert (
|
| 183 |
+
self.disable_norm_outer_blocks >= 0 and self.disable_norm_outer_blocks <= self.n_blocks
|
| 184 |
+
), (
|
| 185 |
+
"Number of blocks for which to disable norm is invalid."
|
| 186 |
+
"It should be lower or equal to the actual number of blocks in the network and greater or equal to 0."
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
act = getattr(nn, activation)
|
| 190 |
+
mult = 1
|
| 191 |
+
model: tp.List[nn.Module] = [
|
| 192 |
+
StreamingConv1d(
|
| 193 |
+
channels,
|
| 194 |
+
mult * n_filters,
|
| 195 |
+
kernel_size,
|
| 196 |
+
norm="none" if self.disable_norm_outer_blocks >= 1 else norm,
|
| 197 |
+
norm_kwargs=norm_params,
|
| 198 |
+
causal=causal,
|
| 199 |
+
pad_mode=pad_mode,
|
| 200 |
+
)
|
| 201 |
+
]
|
| 202 |
+
if mask_fn is not None and mask_position == 0:
|
| 203 |
+
model += [mask_fn]
|
| 204 |
+
# Downsample to raw audio scale
|
| 205 |
+
for i, ratio in enumerate(self.ratios):
|
| 206 |
+
block_norm = "none" if self.disable_norm_outer_blocks >= i + 2 else norm
|
| 207 |
+
# Add residual layers
|
| 208 |
+
for j in range(n_residual_layers):
|
| 209 |
+
model += [
|
| 210 |
+
SEANetResnetBlock(
|
| 211 |
+
mult * n_filters,
|
| 212 |
+
kernel_sizes=[residual_kernel_size, 1],
|
| 213 |
+
dilations=[dilation_base**j, 1],
|
| 214 |
+
norm=block_norm,
|
| 215 |
+
norm_params=norm_params,
|
| 216 |
+
activation=activation,
|
| 217 |
+
activation_params=activation_params,
|
| 218 |
+
causal=causal,
|
| 219 |
+
pad_mode=pad_mode,
|
| 220 |
+
compress=compress,
|
| 221 |
+
true_skip=true_skip,
|
| 222 |
+
)
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
# Add downsampling layers
|
| 226 |
+
model += [
|
| 227 |
+
act(**activation_params),
|
| 228 |
+
StreamingConv1d(
|
| 229 |
+
mult * n_filters,
|
| 230 |
+
mult * n_filters * 2,
|
| 231 |
+
kernel_size=ratio * 2,
|
| 232 |
+
stride=ratio,
|
| 233 |
+
norm=block_norm,
|
| 234 |
+
norm_kwargs=norm_params,
|
| 235 |
+
causal=causal,
|
| 236 |
+
pad_mode=pad_mode,
|
| 237 |
+
),
|
| 238 |
+
]
|
| 239 |
+
mult *= 2
|
| 240 |
+
if mask_fn is not None and mask_position == i + 1:
|
| 241 |
+
model += [mask_fn]
|
| 242 |
+
|
| 243 |
+
model += [
|
| 244 |
+
act(**activation_params),
|
| 245 |
+
StreamingConv1d(
|
| 246 |
+
mult * n_filters,
|
| 247 |
+
dimension,
|
| 248 |
+
last_kernel_size,
|
| 249 |
+
norm=(
|
| 250 |
+
"none" if self.disable_norm_outer_blocks == self.n_blocks else norm
|
| 251 |
+
),
|
| 252 |
+
norm_kwargs=norm_params,
|
| 253 |
+
causal=causal,
|
| 254 |
+
pad_mode=pad_mode,
|
| 255 |
+
),
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
self.model = nn.Sequential(*model)
|
| 259 |
+
|
| 260 |
+
@torch_compile_lazy
|
| 261 |
+
def forward(self, x):
|
| 262 |
+
return self.model(x)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class SEANetDecoder(StreamingContainer):
|
| 266 |
+
"""SEANet decoder.
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
channels (int): Audio channels.
|
| 270 |
+
dimension (int): Intermediate representation dimension.
|
| 271 |
+
n_filters (int): Base width for the model.
|
| 272 |
+
n_residual_layers (int): nb of residual layers.
|
| 273 |
+
ratios (Sequence[int]): kernel size and stride ratios.
|
| 274 |
+
activation (str): Activation function.
|
| 275 |
+
activation_params (dict): Parameters to provide to the activation function.
|
| 276 |
+
final_activation (str): Final activation function after all convolutions.
|
| 277 |
+
final_activation_params (dict): Parameters to provide to the activation function.
|
| 278 |
+
norm (str): Normalization method.
|
| 279 |
+
norm_params (dict): Parameters to provide to the underlying normalization used along with the convolution.
|
| 280 |
+
kernel_size (int): Kernel size for the initial convolution.
|
| 281 |
+
last_kernel_size (int): Kernel size for the initial convolution.
|
| 282 |
+
residual_kernel_size (int): Kernel size for the residual layers.
|
| 283 |
+
dilation_base (int): How much to increase the dilation with each layer.
|
| 284 |
+
causal (bool): Whether to use fully causal convolution.
|
| 285 |
+
pad_mode (str): Padding mode for the convolutions.
|
| 286 |
+
true_skip (bool): Whether to use true skip connection or a simple.
|
| 287 |
+
(streamable) convolution as the skip connection in the residual network blocks.
|
| 288 |
+
compress (int): Reduced dimensionality in residual branches (from Demucs v3).
|
| 289 |
+
disable_norm_outer_blocks (int): Number of blocks for which we don't apply norm.
|
| 290 |
+
For the decoder, it corresponds to the N last blocks.
|
| 291 |
+
trim_right_ratio (float): Ratio for trimming at the right of the transposed convolution under the causal setup.
|
| 292 |
+
If equal to 1.0, it means that all the trimming is done at the right.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
def __init__(
|
| 296 |
+
self,
|
| 297 |
+
channels: int = 1,
|
| 298 |
+
dimension: int = 128,
|
| 299 |
+
n_filters: int = 32,
|
| 300 |
+
n_residual_layers: int = 3,
|
| 301 |
+
ratios: tp.List[int] = [8, 5, 4, 2],
|
| 302 |
+
activation: str = "ELU",
|
| 303 |
+
activation_params: dict = {"alpha": 1.0},
|
| 304 |
+
final_activation: tp.Optional[str] = None,
|
| 305 |
+
final_activation_params: tp.Optional[dict] = None,
|
| 306 |
+
norm: str = "none",
|
| 307 |
+
norm_params: tp.Dict[str, tp.Any] = {},
|
| 308 |
+
kernel_size: int = 7,
|
| 309 |
+
last_kernel_size: int = 7,
|
| 310 |
+
residual_kernel_size: int = 3,
|
| 311 |
+
dilation_base: int = 2,
|
| 312 |
+
causal: bool = False,
|
| 313 |
+
pad_mode: str = "reflect",
|
| 314 |
+
true_skip: bool = True,
|
| 315 |
+
compress: int = 2,
|
| 316 |
+
disable_norm_outer_blocks: int = 0,
|
| 317 |
+
trim_right_ratio: float = 1.0,
|
| 318 |
+
):
|
| 319 |
+
super().__init__()
|
| 320 |
+
self.dimension = dimension
|
| 321 |
+
self.channels = channels
|
| 322 |
+
self.n_filters = n_filters
|
| 323 |
+
self.ratios = ratios
|
| 324 |
+
del ratios
|
| 325 |
+
self.n_residual_layers = n_residual_layers
|
| 326 |
+
self.hop_length = int(np.prod(self.ratios))
|
| 327 |
+
self.n_blocks = len(self.ratios) + 2 # first and last conv + residual blocks
|
| 328 |
+
self.disable_norm_outer_blocks = disable_norm_outer_blocks
|
| 329 |
+
assert (
|
| 330 |
+
self.disable_norm_outer_blocks >= 0 and self.disable_norm_outer_blocks <= self.n_blocks
|
| 331 |
+
), (
|
| 332 |
+
"Number of blocks for which to disable norm is invalid."
|
| 333 |
+
"It should be lower or equal to the actual number of blocks in the network and greater or equal to 0."
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
act = getattr(nn, activation)
|
| 337 |
+
mult = int(2 ** len(self.ratios))
|
| 338 |
+
model: tp.List[nn.Module] = [
|
| 339 |
+
StreamingConv1d(
|
| 340 |
+
dimension,
|
| 341 |
+
mult * n_filters,
|
| 342 |
+
kernel_size,
|
| 343 |
+
norm=(
|
| 344 |
+
"none" if self.disable_norm_outer_blocks == self.n_blocks else norm
|
| 345 |
+
),
|
| 346 |
+
norm_kwargs=norm_params,
|
| 347 |
+
causal=causal,
|
| 348 |
+
pad_mode=pad_mode,
|
| 349 |
+
)
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
# Upsample to raw audio scale
|
| 353 |
+
for i, ratio in enumerate(self.ratios):
|
| 354 |
+
block_norm = (
|
| 355 |
+
"none"
|
| 356 |
+
if self.disable_norm_outer_blocks >= self.n_blocks - (i + 1)
|
| 357 |
+
else norm
|
| 358 |
+
)
|
| 359 |
+
# Add upsampling layers
|
| 360 |
+
model += [
|
| 361 |
+
act(**activation_params),
|
| 362 |
+
StreamingConvTranspose1d(
|
| 363 |
+
mult * n_filters,
|
| 364 |
+
mult * n_filters // 2,
|
| 365 |
+
kernel_size=ratio * 2,
|
| 366 |
+
stride=ratio,
|
| 367 |
+
norm=block_norm,
|
| 368 |
+
norm_kwargs=norm_params,
|
| 369 |
+
causal=causal,
|
| 370 |
+
trim_right_ratio=trim_right_ratio,
|
| 371 |
+
),
|
| 372 |
+
]
|
| 373 |
+
# Add residual layers
|
| 374 |
+
for j in range(n_residual_layers):
|
| 375 |
+
model += [
|
| 376 |
+
SEANetResnetBlock(
|
| 377 |
+
mult * n_filters // 2,
|
| 378 |
+
kernel_sizes=[residual_kernel_size, 1],
|
| 379 |
+
dilations=[dilation_base**j, 1],
|
| 380 |
+
activation=activation,
|
| 381 |
+
activation_params=activation_params,
|
| 382 |
+
norm=block_norm,
|
| 383 |
+
norm_params=norm_params,
|
| 384 |
+
causal=causal,
|
| 385 |
+
pad_mode=pad_mode,
|
| 386 |
+
compress=compress,
|
| 387 |
+
true_skip=true_skip,
|
| 388 |
+
)
|
| 389 |
+
]
|
| 390 |
+
|
| 391 |
+
mult //= 2
|
| 392 |
+
|
| 393 |
+
# Add final layers
|
| 394 |
+
model += [
|
| 395 |
+
act(**activation_params),
|
| 396 |
+
StreamingConv1d(
|
| 397 |
+
n_filters,
|
| 398 |
+
channels,
|
| 399 |
+
last_kernel_size,
|
| 400 |
+
norm="none" if self.disable_norm_outer_blocks >= 1 else norm,
|
| 401 |
+
norm_kwargs=norm_params,
|
| 402 |
+
causal=causal,
|
| 403 |
+
pad_mode=pad_mode,
|
| 404 |
+
),
|
| 405 |
+
]
|
| 406 |
+
# Add optional final activation to decoder (eg. tanh)
|
| 407 |
+
if final_activation is not None:
|
| 408 |
+
final_act = getattr(nn, final_activation)
|
| 409 |
+
final_activation_params = final_activation_params or {}
|
| 410 |
+
model += [final_act(**final_activation_params)]
|
| 411 |
+
self.model = nn.Sequential(*model)
|
| 412 |
+
|
| 413 |
+
@torch_compile_lazy
|
| 414 |
+
def forward(self, z):
|
| 415 |
+
y = self.model(z)
|
| 416 |
+
return y
|
moshi/modules/streaming.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
"""
|
| 33 |
+
Streaming module API that should be implemented by all Streaming components,
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
import abc
|
| 37 |
+
from contextlib import contextmanager
|
| 38 |
+
from dataclasses import dataclass, fields, is_dataclass
|
| 39 |
+
import itertools
|
| 40 |
+
import math
|
| 41 |
+
import json
|
| 42 |
+
from typing import List, Union, Protocol, TypeVar, Generic, Any, Optional
|
| 43 |
+
import torch
|
| 44 |
+
from safetensors.torch import save_file, load_file
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Resetable(Protocol):
|
| 48 |
+
def reset(self) -> None:
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
State = TypeVar("State", bound=Resetable)
|
| 53 |
+
StreamingStateDict = dict[str, Union[torch.Tensor, int, float, str, bool, None]]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def is_dataclass_instance(obj):
|
| 57 |
+
"""Check if obj is an instance of a dataclass (not the class itself).
|
| 58 |
+
|
| 59 |
+
Parameters
|
| 60 |
+
----------
|
| 61 |
+
obj : Any
|
| 62 |
+
Object to check.
|
| 63 |
+
|
| 64 |
+
Returns
|
| 65 |
+
-------
|
| 66 |
+
bool
|
| 67 |
+
True if obj is an instance of a dataclass, False otherwise.
|
| 68 |
+
"""
|
| 69 |
+
return is_dataclass(obj) and not isinstance(obj, type)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _restore_streaming_state_pt(value: torch.Tensor,
|
| 73 |
+
name: str,
|
| 74 |
+
state_dict: dict[str, torch.Tensor],
|
| 75 |
+
):
|
| 76 |
+
"""Restore the streaming state from the given pt_state dict
|
| 77 |
+
|
| 78 |
+
Parameters
|
| 79 |
+
----------
|
| 80 |
+
value : torch.Tensor
|
| 81 |
+
Specific streaming state tensor that needs to be set.
|
| 82 |
+
name : str
|
| 83 |
+
Name of the tensor in the state dict.
|
| 84 |
+
state_dict: StreamingStateDict
|
| 85 |
+
Flattened state dict containing the values to set.
|
| 86 |
+
"""
|
| 87 |
+
if name in state_dict:
|
| 88 |
+
value.copy_(state_dict[name].to(value.device))
|
| 89 |
+
state_dict.pop(name)
|
| 90 |
+
else:
|
| 91 |
+
raise KeyError(f"Expected to find a streaming state for {name}.")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _set_streaming_state_inplace(streaming_state: State,
|
| 95 |
+
state_dict: StreamingStateDict,
|
| 96 |
+
prefix: str,
|
| 97 |
+
device: torch.device,
|
| 98 |
+
):
|
| 99 |
+
"""Set the streaming state in-place from the given `state_dict` dict.
|
| 100 |
+
|
| 101 |
+
Parameters
|
| 102 |
+
----------
|
| 103 |
+
streaming_state : State
|
| 104 |
+
Specific streaming state object that needs to be set.
|
| 105 |
+
state_dict: StreamingStateDict
|
| 106 |
+
Flattened state dict containing the values to set.
|
| 107 |
+
prefix : str
|
| 108 |
+
Prefix to add to each key when looking up in `state_dict`.
|
| 109 |
+
device : torch.device
|
| 110 |
+
Device to move tensors to if needed.
|
| 111 |
+
"""
|
| 112 |
+
if isinstance(streaming_state, torch.Tensor):
|
| 113 |
+
_restore_streaming_state_pt(streaming_state, prefix, state_dict)
|
| 114 |
+
elif is_dataclass_instance(streaming_state):
|
| 115 |
+
_restore_streaming_state_from_keys(streaming_state, state_dict, prefix, [field.name for field in fields(streaming_state)], device)
|
| 116 |
+
elif hasattr(streaming_state, "asdict"):
|
| 117 |
+
_restore_streaming_state_from_keys(streaming_state, state_dict, prefix, list(streaming_state.asdict().keys()), device)
|
| 118 |
+
else:
|
| 119 |
+
raise TypeError(f"Unsupported type {type(streaming_state)} for streaming state with prefix {prefix}.")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _restore_streaming_state_from_keys(streaming_state: State,
|
| 123 |
+
state_dict: StreamingStateDict,
|
| 124 |
+
prefix: str,
|
| 125 |
+
keys: List[str],
|
| 126 |
+
device: torch.device,
|
| 127 |
+
):
|
| 128 |
+
"""Restores the streaming state from the given `state_dict` dict
|
| 129 |
+
looking up fields by adding `prefix` to each key in `keys` to look
|
| 130 |
+
up values.
|
| 131 |
+
`torch.Tensor` are copied to `device` if no `torch.Tensor` is already present
|
| 132 |
+
otherwise, the data is copied to the device of the existing `torch.Tensor`.
|
| 133 |
+
|
| 134 |
+
Parameters
|
| 135 |
+
----------
|
| 136 |
+
streaming_state : State
|
| 137 |
+
Specific streaming state object that needs to be set.
|
| 138 |
+
state_dict: StreamingStateDict
|
| 139 |
+
Flattened state dict containing the values to set.
|
| 140 |
+
prefix : str
|
| 141 |
+
Prefix to add to each key when looking up in `state_dict`.
|
| 142 |
+
keys : List[str]
|
| 143 |
+
List of keys to look up in `state_dict`.
|
| 144 |
+
device : torch.device
|
| 145 |
+
Device to move tensors to if needed.
|
| 146 |
+
"""
|
| 147 |
+
for key in keys:
|
| 148 |
+
full_key = f"{prefix}.{key}"
|
| 149 |
+
existing_value = getattr(streaming_state, key)
|
| 150 |
+
if isinstance(existing_value, torch.Tensor):
|
| 151 |
+
_restore_streaming_state_pt(existing_value, full_key, state_dict)
|
| 152 |
+
elif isinstance(existing_value, (int, float, str, bool, type(None))):
|
| 153 |
+
if full_key in state_dict:
|
| 154 |
+
restored_value = state_dict[full_key]
|
| 155 |
+
if isinstance(restored_value, torch.Tensor):
|
| 156 |
+
restored_value = restored_value.to(device)
|
| 157 |
+
setattr(streaming_state, key, restored_value)
|
| 158 |
+
|
| 159 |
+
state_dict.pop(full_key)
|
| 160 |
+
else:
|
| 161 |
+
raise RuntimeError(f"Key {full_key} not found in state_dict.")
|
| 162 |
+
else:
|
| 163 |
+
_set_streaming_state_inplace(existing_value, state_dict, full_key, device)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def safe_asdict(dataclass_obj):
|
| 167 |
+
"""
|
| 168 |
+
safe_asdict(dataclass_obj)
|
| 169 |
+
|
| 170 |
+
Converts a dataclass object to a dict, skipping empty nested
|
| 171 |
+
dataclasses without requiring values to be pickleable.
|
| 172 |
+
|
| 173 |
+
Parameters
|
| 174 |
+
----------
|
| 175 |
+
dataclass_obj : Any
|
| 176 |
+
Dataclass object to convert.
|
| 177 |
+
|
| 178 |
+
Returns
|
| 179 |
+
-------
|
| 180 |
+
dict
|
| 181 |
+
Dictionary representation of the dataclass object.
|
| 182 |
+
"""
|
| 183 |
+
out = {}
|
| 184 |
+
for field in fields(dataclass_obj):
|
| 185 |
+
value = getattr(dataclass_obj, field.name)
|
| 186 |
+
if is_dataclass_instance(value):
|
| 187 |
+
subvalue = safe_asdict(value)
|
| 188 |
+
if len(subvalue) > 0:
|
| 189 |
+
out[field.name] = subvalue
|
| 190 |
+
else:
|
| 191 |
+
out[field.name] = value
|
| 192 |
+
return out
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _flatten_streaming_state(state_dict: dict[str, torch.Tensor],
|
| 196 |
+
state_dict_metadata: dict[str, Union[int, float, str, None]],
|
| 197 |
+
state: dict[str, State],
|
| 198 |
+
prefix: str,
|
| 199 |
+
):
|
| 200 |
+
"""
|
| 201 |
+
_flatten_streaming_state(state_dict, state_dict_metadata, state, prefix)
|
| 202 |
+
|
| 203 |
+
Helper function for recursively flattening the streaming state into a dict of tensors
|
| 204 |
+
and a dict of metadata (non-tensor values).
|
| 205 |
+
|
| 206 |
+
Parameters
|
| 207 |
+
----------
|
| 208 |
+
state_dict : dict[str, torch.Tensor]
|
| 209 |
+
Dictionary to store the flattened tensor states.
|
| 210 |
+
state_dict_metadata : dict[str, Union[int, float, str, None]]
|
| 211 |
+
Dictionary to store the flattened non-tensor states.
|
| 212 |
+
state : dict[str, State]
|
| 213 |
+
The streaming state to flatten.
|
| 214 |
+
prefix : str
|
| 215 |
+
Prefix to add to each key in the flattened state.
|
| 216 |
+
"""
|
| 217 |
+
for key, value in state.items():
|
| 218 |
+
if isinstance(value, torch.Tensor):
|
| 219 |
+
state_dict[f"{prefix}{key}"] = value.contiguous()
|
| 220 |
+
elif is_dataclass_instance(value):
|
| 221 |
+
_flatten_streaming_state(state_dict, state_dict_metadata, safe_asdict(value), prefix=f"{prefix}{key}.")
|
| 222 |
+
elif isinstance(value, dict):
|
| 223 |
+
_flatten_streaming_state(state_dict, state_dict_metadata, value, prefix=f"{prefix}{key}.")
|
| 224 |
+
elif isinstance(value, (str, int, float, bool, type(None))):
|
| 225 |
+
state_dict_metadata[f"{prefix}{key}"] = value
|
| 226 |
+
elif hasattr(value, "asdict"):
|
| 227 |
+
_flatten_streaming_state(state_dict, state_dict_metadata, value.asdict(), prefix=f"{prefix}{key}.")
|
| 228 |
+
else:
|
| 229 |
+
raise TypeError(f"Unsupported type {type(value)} for key {key} (prefix={prefix}) in streaming state.")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def load_streaming_state(path: str,
|
| 233 |
+
metadata_path: str,
|
| 234 |
+
device: Union[str, int] = 'cpu',
|
| 235 |
+
) -> StreamingStateDict:
|
| 236 |
+
"""
|
| 237 |
+
load_streaming_state(path, metadata_path)
|
| 238 |
+
|
| 239 |
+
Loads a streaming state from a safetensors file and its associated metadata json file.
|
| 240 |
+
|
| 241 |
+
Parameters
|
| 242 |
+
----------
|
| 243 |
+
str : path
|
| 244 |
+
Path to the safetensors file.
|
| 245 |
+
str : metadata_path
|
| 246 |
+
Path to the metadata json file.
|
| 247 |
+
device : Union[str, int], optional
|
| 248 |
+
Device to load the tensors onto, by default 'cpu'.
|
| 249 |
+
|
| 250 |
+
Returns
|
| 251 |
+
-------
|
| 252 |
+
dict
|
| 253 |
+
The loaded streaming state flattened as a dictionary.
|
| 254 |
+
"""
|
| 255 |
+
state_dict = load_file(path, device=device)
|
| 256 |
+
with open(metadata_path, "rt", encoding="utf-8") as fin:
|
| 257 |
+
state_dict_metadata = json.load(fin)
|
| 258 |
+
state_dict.update(state_dict_metadata)
|
| 259 |
+
return state_dict
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class StreamingModule(abc.ABC, torch.nn.Module, Generic[State]):
|
| 263 |
+
"""Common API for streaming components.
|
| 264 |
+
|
| 265 |
+
Each streaming component has a streaming state, which is just a dict[str, Tensor].
|
| 266 |
+
By convention, the first dim of each tensor must be the batch size.
|
| 267 |
+
Don't use dots in the key names, as this would clash with submodules
|
| 268 |
+
(like in state_dict).
|
| 269 |
+
|
| 270 |
+
If `self._is_streaming` is True, the component should use and remember
|
| 271 |
+
the proper state inside `self._streaming_state`.
|
| 272 |
+
|
| 273 |
+
To set a streaming component in streaming state, use
|
| 274 |
+
|
| 275 |
+
with module.streaming():
|
| 276 |
+
...
|
| 277 |
+
|
| 278 |
+
This will automatically reset the streaming state when exiting the context manager.
|
| 279 |
+
This also automatically propagates to all streaming children module.
|
| 280 |
+
|
| 281 |
+
Some module might also implement the `StreamingModule.flush` method, although
|
| 282 |
+
this one is trickier, as all parents module must be StreamingModule and implement
|
| 283 |
+
it as well for it to work properly. See `StreamingSequential` after.
|
| 284 |
+
"""
|
| 285 |
+
|
| 286 |
+
def __init__(self) -> None:
|
| 287 |
+
super().__init__()
|
| 288 |
+
self._streaming_state: State | None = None
|
| 289 |
+
self._streaming_propagate: bool = True
|
| 290 |
+
|
| 291 |
+
@property
|
| 292 |
+
def is_streaming(self):
|
| 293 |
+
return self._streaming_state is not None
|
| 294 |
+
|
| 295 |
+
def set_streaming_propagate(self, streaming_propagate: bool):
|
| 296 |
+
self._streaming_propagate = streaming_propagate
|
| 297 |
+
|
| 298 |
+
def _apply_named_streaming(self, fn: Any):
|
| 299 |
+
def _handle_module(prefix: str, module: torch.nn.Module, recurse: bool = True):
|
| 300 |
+
propagate = True
|
| 301 |
+
if isinstance(module, StreamingModule):
|
| 302 |
+
if module._streaming_propagate:
|
| 303 |
+
fn(prefix, module)
|
| 304 |
+
else:
|
| 305 |
+
propagate = False
|
| 306 |
+
if not recurse:
|
| 307 |
+
return
|
| 308 |
+
if propagate:
|
| 309 |
+
for name, child in module.named_children():
|
| 310 |
+
_handle_module(prefix + "." + name, child)
|
| 311 |
+
|
| 312 |
+
_handle_module("", self, recurse=False)
|
| 313 |
+
for name, child in self.named_children():
|
| 314 |
+
_handle_module(name, child)
|
| 315 |
+
|
| 316 |
+
def _start_streaming(self, batch_size: int):
|
| 317 |
+
def _start_streaming(name: str, module: StreamingModule):
|
| 318 |
+
module._streaming_state = module._init_streaming_state(batch_size)
|
| 319 |
+
|
| 320 |
+
self._apply_named_streaming(_start_streaming)
|
| 321 |
+
|
| 322 |
+
def _stop_streaming(self):
|
| 323 |
+
def _stop_streaming(name: str, module: StreamingModule):
|
| 324 |
+
module._streaming_state = None
|
| 325 |
+
|
| 326 |
+
self._apply_named_streaming(_stop_streaming)
|
| 327 |
+
|
| 328 |
+
@abc.abstractmethod
|
| 329 |
+
def _init_streaming_state(self, batch_size: int) -> State: ...
|
| 330 |
+
|
| 331 |
+
def streaming_forever(self, batch_size: int):
|
| 332 |
+
self._start_streaming(batch_size)
|
| 333 |
+
|
| 334 |
+
@contextmanager
|
| 335 |
+
def streaming(self, batch_size: int):
|
| 336 |
+
"""Context manager to enter streaming mode. Reset streaming state on exit."""
|
| 337 |
+
|
| 338 |
+
self._start_streaming(batch_size)
|
| 339 |
+
try:
|
| 340 |
+
yield
|
| 341 |
+
finally:
|
| 342 |
+
self._stop_streaming()
|
| 343 |
+
|
| 344 |
+
def reset_streaming(self):
|
| 345 |
+
"""Reset the streaming state."""
|
| 346 |
+
|
| 347 |
+
def _reset(name: str, module: StreamingModule):
|
| 348 |
+
state = module._streaming_state
|
| 349 |
+
if state is None:
|
| 350 |
+
raise ValueError(
|
| 351 |
+
f"Trying to reset streaming, but {name} wasn't streaming."
|
| 352 |
+
)
|
| 353 |
+
state.reset()
|
| 354 |
+
|
| 355 |
+
self._apply_named_streaming(_reset)
|
| 356 |
+
|
| 357 |
+
def get_streaming_state(self) -> dict[str, Any]:
|
| 358 |
+
"""Return the complete streaming state, including that of sub-modules."""
|
| 359 |
+
state: dict[str, Any] = {}
|
| 360 |
+
|
| 361 |
+
def _add(name: str, module: StreamingModule):
|
| 362 |
+
state[name] = module._streaming_state
|
| 363 |
+
|
| 364 |
+
self._apply_named_streaming(_add)
|
| 365 |
+
return state
|
| 366 |
+
|
| 367 |
+
def save_streaming_state(self,
|
| 368 |
+
save_path: str,
|
| 369 |
+
metadata_save_path: str,
|
| 370 |
+
extra_state_dict: Optional[dict[str, torch.Tensor]] = None,
|
| 371 |
+
):
|
| 372 |
+
"""Save the streaming state, including that of sub-modules, to the given paths.
|
| 373 |
+
|
| 374 |
+
Parameters
|
| 375 |
+
----------
|
| 376 |
+
save_path : str
|
| 377 |
+
Path to save the streaming state tensors (safetensors format).
|
| 378 |
+
metadata_save_path : str
|
| 379 |
+
Path to save the streaming state metadata (json format).
|
| 380 |
+
extra_state_dict : Optional[dict[str, torch.Tensor]], optional
|
| 381 |
+
Extra state dict to include in the saved streaming state tensors, by default None.
|
| 382 |
+
"""
|
| 383 |
+
state_dict = {}
|
| 384 |
+
if extra_state_dict is not None:
|
| 385 |
+
state_dict.update(extra_state_dict)
|
| 386 |
+
state_dict_metadata = {}
|
| 387 |
+
state = self.get_streaming_state()
|
| 388 |
+
_flatten_streaming_state(state_dict, state_dict_metadata, state, prefix="")
|
| 389 |
+
save_file(state_dict, save_path)
|
| 390 |
+
with open(metadata_save_path, "wt", encoding="utf-8") as fout:
|
| 391 |
+
json.dump(state_dict_metadata, fout)
|
| 392 |
+
|
| 393 |
+
def set_streaming_state_inplace(self, state: StreamingStateDict):
|
| 394 |
+
"""
|
| 395 |
+
Set the streaming state in-place, including that of
|
| 396 |
+
sub-modules using a flattened-state dict.
|
| 397 |
+
"""
|
| 398 |
+
device = next(self.parameters()).device
|
| 399 |
+
def _set(name: str, module: StreamingModule):
|
| 400 |
+
_set_streaming_state_inplace(module._streaming_state, state, prefix=name, device=device)
|
| 401 |
+
self._apply_named_streaming(_set)
|
| 402 |
+
if state:
|
| 403 |
+
raise RuntimeError(f"Some states were not consumed: {list(state.keys())}")
|
| 404 |
+
|
| 405 |
+
def set_streaming_state(self, state: dict[str, Any]):
|
| 406 |
+
"""Set the streaming state, including that of sub-modules."""
|
| 407 |
+
state = dict(state)
|
| 408 |
+
|
| 409 |
+
def _set(name: str, module: StreamingModule):
|
| 410 |
+
if name in state:
|
| 411 |
+
module._streaming_state = state[name]
|
| 412 |
+
state.pop(name)
|
| 413 |
+
else:
|
| 414 |
+
raise RuntimeError(f"Expected to find a streaming state for {name}.")
|
| 415 |
+
|
| 416 |
+
self._apply_named_streaming(_set)
|
| 417 |
+
if state:
|
| 418 |
+
raise RuntimeError(f"Some states were not consumed: {list(state.keys())}")
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
@dataclass
|
| 422 |
+
class _NullState:
|
| 423 |
+
pass
|
| 424 |
+
|
| 425 |
+
def reset(self) -> None:
|
| 426 |
+
pass
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
class StreamingContainer(StreamingModule[_NullState]):
|
| 430 |
+
def _init_streaming_state(self, batch_size: int) -> _NullState:
|
| 431 |
+
return _NullState()
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
@dataclass
|
| 435 |
+
class _StreamingAddState:
|
| 436 |
+
previous_x: torch.Tensor | None = None
|
| 437 |
+
previous_y: torch.Tensor | None = None
|
| 438 |
+
|
| 439 |
+
def reset(self):
|
| 440 |
+
self.previous_x = None
|
| 441 |
+
self.previous_y = None
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
class StreamingAdd(StreamingModule[_StreamingAddState]):
|
| 445 |
+
def _init_streaming_state(self, batch_size: int) -> _StreamingAddState:
|
| 446 |
+
return _StreamingAddState()
|
| 447 |
+
|
| 448 |
+
def forward(self, x: torch.Tensor, y: torch.Tensor):
|
| 449 |
+
if self._streaming_state is None:
|
| 450 |
+
return x + y
|
| 451 |
+
else:
|
| 452 |
+
prev_x = self._streaming_state.previous_x
|
| 453 |
+
prev_y = self._streaming_state.previous_y
|
| 454 |
+
if prev_x is not None:
|
| 455 |
+
x = torch.cat([prev_x, x], dim=-1)
|
| 456 |
+
if prev_y is not None:
|
| 457 |
+
y = torch.cat([prev_y, y], dim=-1)
|
| 458 |
+
m_l = min(x.shape[-1], y.shape[-1])
|
| 459 |
+
self._streaming_state.previous_x = x[..., m_l:]
|
| 460 |
+
self._streaming_state.previous_y = y[..., m_l:]
|
| 461 |
+
return x[..., :m_l] + y[..., :m_l]
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
@dataclass
|
| 465 |
+
class _StreamingConvState:
|
| 466 |
+
previous: torch.Tensor | None = None
|
| 467 |
+
|
| 468 |
+
def reset(self):
|
| 469 |
+
self.previous = None
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
class RawStreamingConv1d(torch.nn.Conv1d, StreamingModule[_StreamingConvState]):
|
| 473 |
+
def __init__(self, *args, **kwargs):
|
| 474 |
+
super().__init__(*args, **kwargs)
|
| 475 |
+
assert self.padding[0] == 0, "Padding should be handled outside."
|
| 476 |
+
assert (
|
| 477 |
+
self.stride[0] <= self.kernel_size[0]
|
| 478 |
+
), "stride must be less than kernel_size."
|
| 479 |
+
|
| 480 |
+
def _init_streaming_state(self, batch_size: int) -> _StreamingConvState:
|
| 481 |
+
return _StreamingConvState()
|
| 482 |
+
|
| 483 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 484 |
+
stride = self.stride[0]
|
| 485 |
+
# Effective kernel size accounting for dilation.
|
| 486 |
+
kernel = (self.kernel_size[0] - 1) * self.dilation[0] + 1
|
| 487 |
+
if self._streaming_state is None:
|
| 488 |
+
return super().forward(input)
|
| 489 |
+
else:
|
| 490 |
+
# Due to the potential overlap, we might have some cache of the previous time steps.
|
| 491 |
+
previous = self._streaming_state.previous
|
| 492 |
+
if previous is not None:
|
| 493 |
+
input = torch.cat([previous, input], dim=-1)
|
| 494 |
+
B, C, T = input.shape
|
| 495 |
+
# We now compute the number of full convolution frames, i.e. the frames
|
| 496 |
+
# that are ready to be computed.
|
| 497 |
+
num_frames = max(0, int(math.floor((T - kernel) / stride) + 1))
|
| 498 |
+
offset = num_frames * stride
|
| 499 |
+
# We will compute `num_frames` outputs, and we are advancing by `stride`
|
| 500 |
+
# for each of the frame, so we know the data before `stride * num_frames`
|
| 501 |
+
# will never be used again.
|
| 502 |
+
self._streaming_state.previous = input[..., offset:]
|
| 503 |
+
if num_frames > 0:
|
| 504 |
+
input_length = (num_frames - 1) * stride + kernel
|
| 505 |
+
out = super().forward(input[..., :input_length])
|
| 506 |
+
else:
|
| 507 |
+
# Not enough data as this point to output some new frames.
|
| 508 |
+
out = torch.empty(
|
| 509 |
+
B, self.out_channels, 0, device=input.device, dtype=input.dtype
|
| 510 |
+
)
|
| 511 |
+
return out
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
@dataclass
|
| 515 |
+
class _StreamingConvTrState:
|
| 516 |
+
partial: torch.Tensor | None = None
|
| 517 |
+
|
| 518 |
+
def reset(self):
|
| 519 |
+
self.partial = None
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
class RawStreamingConvTranspose1d(
|
| 523 |
+
torch.nn.ConvTranspose1d, StreamingModule[_StreamingConvTrState]
|
| 524 |
+
):
|
| 525 |
+
def __init__(self, *args, **kwargs):
|
| 526 |
+
super().__init__(*args, **kwargs)
|
| 527 |
+
assert self.padding[0] == 0, "Padding should be handled outside."
|
| 528 |
+
assert self.dilation[0] == 1, "No dilation for now"
|
| 529 |
+
assert (
|
| 530 |
+
self.stride[0] <= self.kernel_size[0]
|
| 531 |
+
), "stride must be less than kernel_size."
|
| 532 |
+
assert self.output_padding[0] == 0, "Output padding not supported."
|
| 533 |
+
|
| 534 |
+
def _init_streaming_state(self, batch_size: int) -> _StreamingConvTrState:
|
| 535 |
+
return _StreamingConvTrState()
|
| 536 |
+
|
| 537 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor: # type: ignore
|
| 538 |
+
B, C, T = x.shape
|
| 539 |
+
stride = self.stride[0]
|
| 540 |
+
kernel = self.kernel_size[0]
|
| 541 |
+
if self._streaming_state is None:
|
| 542 |
+
return super().forward(x)
|
| 543 |
+
else:
|
| 544 |
+
if T == 0:
|
| 545 |
+
return torch.empty(
|
| 546 |
+
B, self.out_channels, 0, device=x.device, dtype=x.dtype
|
| 547 |
+
)
|
| 548 |
+
out = super().forward(x)
|
| 549 |
+
OT = out.shape[-1]
|
| 550 |
+
partial = self._streaming_state.partial
|
| 551 |
+
if partial is not None:
|
| 552 |
+
# Due to the potential overlap, the rightmost output of the conv transpose is not
|
| 553 |
+
# ready to be output, as it will receive contributions from the next input frames.
|
| 554 |
+
# Here we recover those `partial` output frames. We know that the first time step
|
| 555 |
+
# of the `partial` tensor corresponds to the first time step of `out` as anything
|
| 556 |
+
# coming before the first time step of `out` would have been already flushed.
|
| 557 |
+
PT = partial.shape[-1]
|
| 558 |
+
if self.bias is not None:
|
| 559 |
+
out[..., :PT] += partial - self.bias[:, None]
|
| 560 |
+
else:
|
| 561 |
+
out[..., :PT] += partial
|
| 562 |
+
# The input is T, the output is S * (T - 1) + K.
|
| 563 |
+
# The offset of the left of the next frame will be S * T
|
| 564 |
+
# so everything between 0 and S * T is ready to be output, and we need
|
| 565 |
+
# to keep in the internal state everything beyond that, i.e. S (T - 1) + K - S T = K - S
|
| 566 |
+
invalid_steps = kernel - stride
|
| 567 |
+
partial = out[..., OT - invalid_steps :]
|
| 568 |
+
out = out[..., : OT - invalid_steps]
|
| 569 |
+
self._streaming_state.partial = partial
|
| 570 |
+
return out
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def test():
|
| 574 |
+
torch.manual_seed(1234)
|
| 575 |
+
device = "cpu"
|
| 576 |
+
if torch.cuda.is_available():
|
| 577 |
+
# Avoid the cuda optimizations that would take place on single precision
|
| 578 |
+
# floats for convolutions.
|
| 579 |
+
torch.backends.cudnn.enabled = True
|
| 580 |
+
torch.backends.cudnn.benchmark = False
|
| 581 |
+
torch.backends.cudnn.deterministic = True
|
| 582 |
+
torch.backends.cuda.matmul.allow_tf32 = False
|
| 583 |
+
torch.backends.cudnn.allow_tf32 = False
|
| 584 |
+
device = "cuda:0"
|
| 585 |
+
|
| 586 |
+
kernel_sizes = [1, 3, 4, 8, 15, 16]
|
| 587 |
+
strides = [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
| 588 |
+
chin = 6
|
| 589 |
+
chout = 12
|
| 590 |
+
|
| 591 |
+
for kernel, stride in itertools.product(kernel_sizes, strides):
|
| 592 |
+
if stride > kernel:
|
| 593 |
+
continue
|
| 594 |
+
conv = RawStreamingConv1d(chin, chout, kernel, stride).to(device)
|
| 595 |
+
convtr = RawStreamingConvTranspose1d(chout, chin, kernel, stride).to(device)
|
| 596 |
+
|
| 597 |
+
for length in [4, 8, 32, 54, 65, 128, 1043]:
|
| 598 |
+
print(f"ksize {kernel} strides {stride} len {length}")
|
| 599 |
+
if length < kernel:
|
| 600 |
+
continue
|
| 601 |
+
batch_size = 3
|
| 602 |
+
x = torch.randn(batch_size, chin, length).to(device)
|
| 603 |
+
y = conv(x)
|
| 604 |
+
z = convtr(y)
|
| 605 |
+
for chunk_size in [1, 3, 5, 8]:
|
| 606 |
+
ys = []
|
| 607 |
+
zs = []
|
| 608 |
+
with conv.streaming(batch_size), convtr.streaming(batch_size):
|
| 609 |
+
for offset in range(0, length, chunk_size):
|
| 610 |
+
chunk = x[..., offset : offset + chunk_size]
|
| 611 |
+
ys.append(conv(chunk))
|
| 612 |
+
zs.append(convtr(ys[-1]))
|
| 613 |
+
y_stream = torch.cat(ys, dim=-1)
|
| 614 |
+
z_stream = torch.cat(zs, dim=-1)
|
| 615 |
+
y = y[..., : y_stream.shape[-1]]
|
| 616 |
+
z = z[..., : z_stream.shape[-1]]
|
| 617 |
+
assert y.shape == y_stream.shape, (y.shape, y_stream.shape)
|
| 618 |
+
delta = (y_stream - y).norm() / y.norm()
|
| 619 |
+
assert delta <= 1e-6, delta
|
| 620 |
+
num_frames = int((length - kernel) / stride) + 1
|
| 621 |
+
assert num_frames == y_stream.shape[-1]
|
| 622 |
+
|
| 623 |
+
assert z.shape == z_stream.shape, (z.shape, z_stream.shape)
|
| 624 |
+
delta = (z_stream - z).norm() / z.norm()
|
| 625 |
+
assert delta <= 1e-6, (delta, (z_stream - z).abs().mean(dim=(0, 1)))
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
if __name__ == "__main__":
|
| 629 |
+
with torch.no_grad():
|
| 630 |
+
test()
|
moshi/modules/transformer.py
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
Transformer model, with streaming support, + CUDA Graphable.
|
| 28 |
+
Optimized for inference.
|
| 29 |
+
|
| 30 |
+
See `StreamingTransformer` for more information.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from contextlib import ExitStack
|
| 34 |
+
from dataclasses import dataclass
|
| 35 |
+
import typing as tp
|
| 36 |
+
|
| 37 |
+
from einops import rearrange
|
| 38 |
+
import torch
|
| 39 |
+
import torch.nn as nn
|
| 40 |
+
from torch.nn import functional as F
|
| 41 |
+
|
| 42 |
+
from ..utils.compile import no_compile
|
| 43 |
+
from .gating import make_gating
|
| 44 |
+
from .rope import RotaryEmbedding
|
| 45 |
+
from .streaming import StreamingModule, StreamingContainer
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class LayerNormF32(nn.LayerNorm):
|
| 49 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 50 |
+
x_f32 = input.float()
|
| 51 |
+
out_f32 = super().forward(x_f32)
|
| 52 |
+
return out_f32.to(input.dtype)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _rms_norm(
|
| 56 |
+
x: torch.Tensor,
|
| 57 |
+
alpha: torch.Tensor,
|
| 58 |
+
dtype: tp.Optional[torch.dtype],
|
| 59 |
+
eps: float,
|
| 60 |
+
):
|
| 61 |
+
assert x.dim() == 3, f"RMSNorm expects 3D inputs but got {x.shape}"
|
| 62 |
+
x_dtype = x.dtype
|
| 63 |
+
if dtype is not None:
|
| 64 |
+
x = x.to(dtype)
|
| 65 |
+
var = eps + torch.mean(x**2, dim=2, keepdim=True)
|
| 66 |
+
y = (x * (alpha.to(var) * torch.rsqrt(var))).to(x_dtype)
|
| 67 |
+
return y
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class RMSNorm(nn.Module):
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
dim: int,
|
| 74 |
+
eps: float = 1e-5,
|
| 75 |
+
dtype: tp.Optional[torch.dtype] = None,
|
| 76 |
+
device=None,
|
| 77 |
+
):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.eps = eps
|
| 80 |
+
self.dtype = dtype
|
| 81 |
+
self.alpha = nn.Parameter(
|
| 82 |
+
torch.full((1, 1, dim), 1.0, requires_grad=True, device=device, dtype=dtype)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def forward(self, x: torch.Tensor):
|
| 86 |
+
return _rms_norm(x, self.alpha, self.dtype, self.eps)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class LayerScale(nn.Module):
|
| 90 |
+
"""Layer scale from [Touvron et al 2021] (https://arxiv.org/pdf/2103.17239.pdf).
|
| 91 |
+
This rescales diagonally the residual outputs close to 0, with a learnt scale.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
channels (int): Number of channels.
|
| 95 |
+
init (float): Initial scale.
|
| 96 |
+
channel_last (bool): If True, expect `[*, C]` shaped tensors, otherwise, `[*, C, T]`.
|
| 97 |
+
device (torch.device or str, optional): Device on which to initialize the module.
|
| 98 |
+
dtype (torch.dtype, optional): dtype to use to initialize the module.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(
|
| 102 |
+
self,
|
| 103 |
+
channels: int,
|
| 104 |
+
init: float = 1e-4,
|
| 105 |
+
channel_last: bool = True,
|
| 106 |
+
device=None,
|
| 107 |
+
dtype=None,
|
| 108 |
+
):
|
| 109 |
+
super().__init__()
|
| 110 |
+
self.channel_last = channel_last
|
| 111 |
+
self.scale = nn.Parameter(
|
| 112 |
+
torch.full(
|
| 113 |
+
(channels,), init, requires_grad=True, device=device, dtype=dtype
|
| 114 |
+
)
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def forward(self, x: torch.Tensor):
|
| 118 |
+
if self.channel_last:
|
| 119 |
+
return self.scale * x
|
| 120 |
+
else:
|
| 121 |
+
return self.scale[:, None] * x
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def create_norm_fn(norm_type: str, dim: int, **kwargs) -> nn.Module:
|
| 125 |
+
"""Create normalization module for transformer encoder layer.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
norm_type (str): Normalization method.
|
| 129 |
+
dim (int): Dimension of the normalized layer.
|
| 130 |
+
**kwargs (dict): Additional parameters for normalization layer.
|
| 131 |
+
Returns:
|
| 132 |
+
nn.Module: Normalization module.
|
| 133 |
+
"""
|
| 134 |
+
if norm_type == "layer_norm":
|
| 135 |
+
return nn.LayerNorm(dim, eps=1e-5, **kwargs)
|
| 136 |
+
elif norm_type == "layer_norm_f32":
|
| 137 |
+
kwargs.pop("dtype", None)
|
| 138 |
+
return LayerNormF32(dim, eps=1e-8, **kwargs)
|
| 139 |
+
elif norm_type in {"rms_norm"}:
|
| 140 |
+
return RMSNorm(dim, eps=1e-5, **kwargs)
|
| 141 |
+
elif norm_type in {"rms_norm_f32"}:
|
| 142 |
+
kwargs.pop("dtype", None)
|
| 143 |
+
return RMSNorm(dim, eps=1e-8, dtype=torch.float, **kwargs)
|
| 144 |
+
else:
|
| 145 |
+
raise ValueError(f"Unknown norm type: {norm_type}")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def create_sin_embedding(
|
| 149 |
+
positions: torch.Tensor,
|
| 150 |
+
dim: int,
|
| 151 |
+
max_period: float = 10000,
|
| 152 |
+
dtype: torch.dtype = torch.float32,
|
| 153 |
+
) -> torch.Tensor:
|
| 154 |
+
"""Create sinusoidal positional embedding, with shape `[B, T, C]`.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
positions (torch.Tensor): LongTensor of positions.
|
| 158 |
+
dim (int): Dimension of the embedding.
|
| 159 |
+
max_period (float): Maximum period of the cosine/sine functions.
|
| 160 |
+
dtype (torch.dtype or str): dtype to use to generate the embedding.
|
| 161 |
+
Returns:
|
| 162 |
+
torch.Tensor: Sinusoidal positional embedding.
|
| 163 |
+
"""
|
| 164 |
+
# We aim for BTC format
|
| 165 |
+
assert dim % 2 == 0
|
| 166 |
+
half_dim = dim // 2
|
| 167 |
+
positions = positions.to(dtype)
|
| 168 |
+
adim = torch.arange(half_dim, device=positions.device, dtype=dtype).view(1, 1, -1)
|
| 169 |
+
max_period_tensor = torch.full(
|
| 170 |
+
[], max_period, device=positions.device, dtype=dtype
|
| 171 |
+
) # avoid sync point
|
| 172 |
+
phase = positions / (max_period_tensor ** (adim / (half_dim - 1)))
|
| 173 |
+
return torch.cat([torch.cos(phase), torch.sin(phase)], dim=-1)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def multi_linear(
|
| 177 |
+
num_linear: int,
|
| 178 |
+
weight: torch.Tensor,
|
| 179 |
+
x: torch.Tensor,
|
| 180 |
+
offset: int,
|
| 181 |
+
):
|
| 182 |
+
"""Utility to apply a multi linear layer to the given input. A multi linear layer
|
| 183 |
+
applies a different set of weight for each time step.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
num_linear (int): Number of possible time steps and so number of linears.
|
| 187 |
+
weight (torch.Tensor): Weight tensor, with shape `[num_linear * chout, chin]`.
|
| 188 |
+
x (torch.Tensor): Input tensor, with shape `[B, T, C]`.
|
| 189 |
+
offset (int): offset for the current time step, in particular for decoding, with
|
| 190 |
+
time steps provided one by one.
|
| 191 |
+
"""
|
| 192 |
+
B, T, C = x.shape
|
| 193 |
+
ys = []
|
| 194 |
+
chout, chin = weight.shape
|
| 195 |
+
weight = weight.view(num_linear, -1, chin)
|
| 196 |
+
for t in range(T):
|
| 197 |
+
y = F.linear(x[:, t], weight[t + offset])
|
| 198 |
+
ys.append(y)
|
| 199 |
+
out = torch.stack(ys, 1)
|
| 200 |
+
return out
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def set_attention_context(model: nn.Module, context: tp.Optional[int] = None) -> None:
|
| 204 |
+
"""Deactivates or changes the context span (in time steps) in a model.
|
| 205 |
+
Args:
|
| 206 |
+
model (nn.Module): model over which to look for attentions.
|
| 207 |
+
context (int or None): new temporary context value.
|
| 208 |
+
|
| 209 |
+
..Note:: this is not a context manager but a plain function changing the context forever.
|
| 210 |
+
Initially, it was a context manager, but that led to interesting bugs when using
|
| 211 |
+
activation checkpointing, with the context being inconsistent between the forward
|
| 212 |
+
and backward.
|
| 213 |
+
"""
|
| 214 |
+
for module in model.modules():
|
| 215 |
+
if isinstance(module, StreamingMultiheadAttention):
|
| 216 |
+
module.context = context
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class KVCacheResult(tp.NamedTuple):
|
| 220 |
+
keys: torch.Tensor
|
| 221 |
+
values: torch.Tensor
|
| 222 |
+
positions: torch.Tensor
|
| 223 |
+
|
| 224 |
+
@staticmethod
|
| 225 |
+
def from_kv(keys: torch.Tensor, values: torch.Tensor) -> "KVCacheResult":
|
| 226 |
+
B, H, T, D = keys.shape
|
| 227 |
+
assert tuple(values.shape[:-1]) == (B, H, T)
|
| 228 |
+
positions = torch.arange(T, device=keys.device, dtype=torch.long)
|
| 229 |
+
return KVCacheResult(keys, values, positions)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class RingKVCache:
|
| 233 |
+
"""Efficient streaming KVCache to be compatible with Cuda Graph.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
batch_size (int): Batch size.
|
| 237 |
+
num_heads (int): Number of heads in the attention.
|
| 238 |
+
dim_per_head (int): Dimension per head.
|
| 239 |
+
device (torch.device): Device on which to initialize the cache.
|
| 240 |
+
dtype (torch.dtype): dtype to use for the cache.
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
def __init__(
|
| 244 |
+
self,
|
| 245 |
+
batch_size: int,
|
| 246 |
+
num_heads: int,
|
| 247 |
+
dim_per_head: int,
|
| 248 |
+
capacity: int,
|
| 249 |
+
device: torch.device = torch.device("cuda"),
|
| 250 |
+
dtype: torch.dtype = torch.bfloat16,
|
| 251 |
+
):
|
| 252 |
+
self.capacity = capacity
|
| 253 |
+
self.cache = torch.zeros(
|
| 254 |
+
(2, batch_size, num_heads, capacity, dim_per_head),
|
| 255 |
+
device=device,
|
| 256 |
+
dtype=dtype,
|
| 257 |
+
)
|
| 258 |
+
self.end_offset = torch.zeros(1, device=device, dtype=torch.long)
|
| 259 |
+
|
| 260 |
+
def reset(self):
|
| 261 |
+
self.end_offset.zero_()
|
| 262 |
+
|
| 263 |
+
def complete(self, k: torch.Tensor, v: torch.Tensor) -> KVCacheResult:
|
| 264 |
+
assert k.shape[:-1] == v.shape[:-1], (k.shape, v.shape)
|
| 265 |
+
B, H, T, D = k.shape
|
| 266 |
+
indexes = torch.arange(T, device=self.end_offset.device, dtype=self.end_offset.dtype) + self.end_offset
|
| 267 |
+
indexes = indexes % self.capacity
|
| 268 |
+
self.cache[0].index_copy_(2, indexes, k)
|
| 269 |
+
self.cache[1].index_copy_(2, indexes, v)
|
| 270 |
+
self.end_offset.add_(T)
|
| 271 |
+
|
| 272 |
+
keys = self.cache[0]
|
| 273 |
+
values = self.cache[1]
|
| 274 |
+
|
| 275 |
+
indexes = torch.arange(
|
| 276 |
+
self.capacity, device=self.end_offset.device, dtype=torch.long
|
| 277 |
+
)
|
| 278 |
+
invalid = indexes >= self.end_offset
|
| 279 |
+
|
| 280 |
+
end_index = self.end_offset % self.capacity
|
| 281 |
+
delta = indexes - end_index
|
| 282 |
+
|
| 283 |
+
# If last key is for step S, and capacity is C, last key was written at index S % C.
|
| 284 |
+
# then end_offset = S + 1, and end_index = (S + 1) % C.
|
| 285 |
+
# Then for index = (S % C), delta = -1, and the next code gives us:
|
| 286 |
+
# position(index) = (S + 1) - 1 = S, all good.
|
| 287 |
+
# Now the time step at end_offset is actually the oldest in the KVCache, e.g., its
|
| 288 |
+
# position should be (S - self.capacity + 1).
|
| 289 |
+
# The following code gives us:
|
| 290 |
+
# position(index + 1) = S + 1 + 0 - self.capacity.
|
| 291 |
+
|
| 292 |
+
positions = torch.where(
|
| 293 |
+
delta <= 0,
|
| 294 |
+
self.end_offset + delta,
|
| 295 |
+
self.end_offset + delta - self.capacity,
|
| 296 |
+
)
|
| 297 |
+
positions = torch.where(invalid, torch.full_like(positions, -1), positions)
|
| 298 |
+
|
| 299 |
+
return KVCacheResult(keys, values, positions)
|
| 300 |
+
|
| 301 |
+
def asdict(self):
|
| 302 |
+
return {"cache": self.cache, "end_offset": self.end_offset}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
@dataclass
|
| 306 |
+
class _MHAState:
|
| 307 |
+
kv_cache: RingKVCache
|
| 308 |
+
offset: torch.Tensor
|
| 309 |
+
offset_cpu: int
|
| 310 |
+
|
| 311 |
+
def reset(self):
|
| 312 |
+
self.kv_cache.reset()
|
| 313 |
+
self.offset.zero_()
|
| 314 |
+
self.offset_cpu = 0
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
class StreamingMultiheadAttention(StreamingModule[_MHAState]):
|
| 318 |
+
"""Similar to `nn.MultiheadAttention` but with support for streaming, causal evaluation.
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
embed_dim (int): Dimension to project to.
|
| 322 |
+
num_heads (int): Number of heads.
|
| 323 |
+
causal (bool): Causal mask applied automatically.
|
| 324 |
+
context (int, optional): Number of time steps the attention can access to.
|
| 325 |
+
When causal, can access `context` time steps into the past, and when non causal,
|
| 326 |
+
can access `context // 2` steps in the past, and the same in the future.
|
| 327 |
+
rope (`RotaryEmbedding`, optional): Rope embedding to use.
|
| 328 |
+
weights_per_step (int): use different weights per time step. If non zero, should correspond to the
|
| 329 |
+
number of possible time steps.
|
| 330 |
+
device (torch.device, optional): Device on which to initialize.
|
| 331 |
+
dtype (torch.dtype, optional): dtype to use.
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
_fsdp_final = True
|
| 335 |
+
|
| 336 |
+
def __init__(
|
| 337 |
+
self,
|
| 338 |
+
embed_dim: int,
|
| 339 |
+
num_heads: int,
|
| 340 |
+
causal: bool = False,
|
| 341 |
+
context: tp.Optional[int] = None,
|
| 342 |
+
rope: tp.Optional[RotaryEmbedding] = None,
|
| 343 |
+
weights_per_step: int = 0,
|
| 344 |
+
device=None,
|
| 345 |
+
dtype=None,
|
| 346 |
+
):
|
| 347 |
+
super().__init__()
|
| 348 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 349 |
+
|
| 350 |
+
self.embed_dim = embed_dim
|
| 351 |
+
self.causal = causal
|
| 352 |
+
self.context = context
|
| 353 |
+
self.rope = rope
|
| 354 |
+
self.num_heads = num_heads
|
| 355 |
+
|
| 356 |
+
out_dim = embed_dim
|
| 357 |
+
out_dim = 3 * embed_dim
|
| 358 |
+
mult = 1
|
| 359 |
+
self.weights_per_step = weights_per_step
|
| 360 |
+
if weights_per_step:
|
| 361 |
+
mult = weights_per_step
|
| 362 |
+
in_proj = nn.Linear(embed_dim, mult * out_dim, bias=False, **factory_kwargs)
|
| 363 |
+
# We try to follow the default PyTorch MHA convention, to easily compare results.
|
| 364 |
+
self.in_proj_weight = in_proj.weight
|
| 365 |
+
self.in_proj_bias = in_proj.bias
|
| 366 |
+
self.out_proj = nn.Linear(
|
| 367 |
+
embed_dim, mult * embed_dim, bias=False, **factory_kwargs
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
def _init_streaming_state(self, batch_size: int) -> _MHAState:
|
| 371 |
+
if self.context is None:
|
| 372 |
+
if self.weights_per_step:
|
| 373 |
+
capacity = self.weights_per_step
|
| 374 |
+
else:
|
| 375 |
+
raise RuntimeError(
|
| 376 |
+
"Cannot create a streaming KVCache without a context to estimate capacity."
|
| 377 |
+
)
|
| 378 |
+
else:
|
| 379 |
+
capacity = self.context
|
| 380 |
+
device = self.in_proj_weight.device
|
| 381 |
+
# TODO: the following estimation will not work great with FSDP.
|
| 382 |
+
dtype = self.in_proj_weight.dtype
|
| 383 |
+
dim_per_head = self.embed_dim // self.num_heads
|
| 384 |
+
kv_cache = RingKVCache(
|
| 385 |
+
batch_size, self.num_heads, dim_per_head, capacity, device, dtype
|
| 386 |
+
)
|
| 387 |
+
return _MHAState(
|
| 388 |
+
kv_cache,
|
| 389 |
+
offset=torch.zeros(1, device=device, dtype=torch.long),
|
| 390 |
+
offset_cpu=0,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def _complete_kv(self, k, v) -> KVCacheResult:
|
| 394 |
+
state = self._streaming_state
|
| 395 |
+
if state is None:
|
| 396 |
+
return KVCacheResult.from_kv(k, v)
|
| 397 |
+
else:
|
| 398 |
+
return state.kv_cache.complete(k, v)
|
| 399 |
+
|
| 400 |
+
def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor):
|
| 401 |
+
state = self._streaming_state
|
| 402 |
+
T = query.shape[1]
|
| 403 |
+
|
| 404 |
+
if state is None:
|
| 405 |
+
offset = torch.zeros(1, device=query.device, dtype=torch.long)
|
| 406 |
+
offset_cpu = 0
|
| 407 |
+
else:
|
| 408 |
+
assert self.causal, "Streaming only available for causal"
|
| 409 |
+
offset = state.offset
|
| 410 |
+
offset_cpu = state.offset_cpu
|
| 411 |
+
|
| 412 |
+
if self.weights_per_step:
|
| 413 |
+
projected = multi_linear(
|
| 414 |
+
self.weights_per_step, self.in_proj_weight, query, offset_cpu
|
| 415 |
+
)
|
| 416 |
+
else:
|
| 417 |
+
projected = nn.functional.linear(query, self.in_proj_weight)
|
| 418 |
+
q, k, v = rearrange(
|
| 419 |
+
projected, "b t (p h d) -> p b h t d", p=3, h=self.num_heads
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
if self.rope:
|
| 423 |
+
q, k = self.rope(q, k, offset, time_before_heads=False)
|
| 424 |
+
|
| 425 |
+
k, v, pos_k = self._complete_kv(k, v)
|
| 426 |
+
if self.causal:
|
| 427 |
+
pos_k = pos_k.view(1, -1)
|
| 428 |
+
pos_q = offset + torch.arange(T, device=q.device, dtype=torch.long).view(
|
| 429 |
+
-1, 1
|
| 430 |
+
)
|
| 431 |
+
delta = pos_q - pos_k
|
| 432 |
+
attn_bias = (pos_k >= 0) & (delta >= 0)
|
| 433 |
+
if self.context is not None:
|
| 434 |
+
attn_bias = attn_bias & (delta < self.context)
|
| 435 |
+
else:
|
| 436 |
+
attn_bias = None
|
| 437 |
+
x = F.scaled_dot_product_attention(q, k, v, attn_bias, dropout_p=0.0)
|
| 438 |
+
|
| 439 |
+
x = rearrange(x, "b h t d -> b t (h d)")
|
| 440 |
+
if self.weights_per_step:
|
| 441 |
+
x = multi_linear(self.weights_per_step, self.out_proj.weight, x, offset_cpu)
|
| 442 |
+
else:
|
| 443 |
+
x = self.out_proj(x)
|
| 444 |
+
if state is not None:
|
| 445 |
+
state.offset.add_(T)
|
| 446 |
+
state.offset_cpu += T
|
| 447 |
+
return x
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
@dataclass
|
| 451 |
+
class _LayerState:
|
| 452 |
+
offset_cpu: int
|
| 453 |
+
|
| 454 |
+
def reset(self):
|
| 455 |
+
self.offset_cpu = 0
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
class StreamingTransformerLayer(StreamingModule[_LayerState]):
|
| 459 |
+
"""TransformerLayer with Streaming / Causal support.
|
| 460 |
+
|
| 461 |
+
Args:
|
| 462 |
+
d_model (int): Dimension of the data.
|
| 463 |
+
num_heads (int): Number of heads.
|
| 464 |
+
dim_feedforward (int): Intermediate dimension of FF module.
|
| 465 |
+
causal (bool): Causal mask applied automatically.
|
| 466 |
+
context (int, optional): Receptive field for the causal mask, infinite if None.
|
| 467 |
+
custom (bool): Use custom MHA implementation, for testing / benchmarking.
|
| 468 |
+
rope (`RotaryEmbedding`, optional): Rope embedding to use.
|
| 469 |
+
norm (str): Normalization to use. Currently, only 'layer_norm' is supported.
|
| 470 |
+
layer_scale (float, optional): If not None, LayerScale will be used with the given value as initial scale.
|
| 471 |
+
gating (str): if provided, replaces FFN with special gating, like GLU, GSiGLU etc.
|
| 472 |
+
weights_per_step (int): use different weights per time step. If non zero, should correspond to the
|
| 473 |
+
number of possible time steps.
|
| 474 |
+
skip_self_attn: If true, skips the self attention module and the norm
|
| 475 |
+
device (torch.device, optional): Device on which to initialize.
|
| 476 |
+
dtype (torch.dtype, optional): dtype to use.
|
| 477 |
+
"""
|
| 478 |
+
|
| 479 |
+
_fsdp_final = True
|
| 480 |
+
|
| 481 |
+
def __init__(
|
| 482 |
+
self,
|
| 483 |
+
d_model: int,
|
| 484 |
+
num_heads: int,
|
| 485 |
+
dim_feedforward: int | list[int] = 2048,
|
| 486 |
+
causal: bool = False,
|
| 487 |
+
context: tp.Optional[int] = None,
|
| 488 |
+
rope: tp.Optional[RotaryEmbedding] = None,
|
| 489 |
+
norm: str = "layer_norm",
|
| 490 |
+
layer_scale: tp.Optional[float] = None,
|
| 491 |
+
gating: str = "none",
|
| 492 |
+
weights_per_step: int = 0,
|
| 493 |
+
activation=F.gelu,
|
| 494 |
+
skip_self_attn: bool = False,
|
| 495 |
+
device=None,
|
| 496 |
+
dtype=None,
|
| 497 |
+
):
|
| 498 |
+
super().__init__()
|
| 499 |
+
factory_kwargs = {"device": device, "dtype": dtype}
|
| 500 |
+
# Redefine self_attn to our streaming multi-head attention
|
| 501 |
+
attn_kwargs: tp.Dict[str, tp.Any] = {
|
| 502 |
+
"embed_dim": d_model,
|
| 503 |
+
"num_heads": num_heads,
|
| 504 |
+
}
|
| 505 |
+
if not skip_self_attn:
|
| 506 |
+
self.self_attn: StreamingMultiheadAttention = StreamingMultiheadAttention(
|
| 507 |
+
causal=causal,
|
| 508 |
+
context=context,
|
| 509 |
+
rope=rope,
|
| 510 |
+
weights_per_step=weights_per_step,
|
| 511 |
+
**attn_kwargs, # type: ignore
|
| 512 |
+
**factory_kwargs, # type: ignore
|
| 513 |
+
) # type: ignore
|
| 514 |
+
self.norm1 = create_norm_fn(norm, d_model, **factory_kwargs)
|
| 515 |
+
self.norm2 = create_norm_fn(norm, d_model, **factory_kwargs)
|
| 516 |
+
# Redefine feedforward layers to expose bias parameter
|
| 517 |
+
self.weights_per_step = weights_per_step
|
| 518 |
+
self.gating: tp.Optional[nn.Module] = None
|
| 519 |
+
self.linear1: tp.Optional[nn.Module] = None
|
| 520 |
+
self.linear2: tp.Optional[nn.Module] = None
|
| 521 |
+
self.activation = activation
|
| 522 |
+
self.skip_self_attn = skip_self_attn
|
| 523 |
+
|
| 524 |
+
if isinstance(dim_feedforward, list):
|
| 525 |
+
assert dim_feedforward
|
| 526 |
+
assert len(dim_feedforward) == weights_per_step, (
|
| 527 |
+
"Length of dim_feedforward must match weights_per_step,"
|
| 528 |
+
f" got {len(dim_feedforward)} != {weights_per_step}"
|
| 529 |
+
)
|
| 530 |
+
if gating == "none":
|
| 531 |
+
assert (
|
| 532 |
+
not weights_per_step
|
| 533 |
+
), "weights_per_step without gating not supported for now."
|
| 534 |
+
assert not isinstance(
|
| 535 |
+
dim_feedforward, list
|
| 536 |
+
), "List dim_feedforward without gating not supported for now."
|
| 537 |
+
self.linear1 = nn.Linear(
|
| 538 |
+
d_model, dim_feedforward, bias=False, **factory_kwargs
|
| 539 |
+
)
|
| 540 |
+
self.linear2 = nn.Linear(
|
| 541 |
+
dim_feedforward, d_model, bias=False, **factory_kwargs
|
| 542 |
+
)
|
| 543 |
+
else:
|
| 544 |
+
self.linear1 = None
|
| 545 |
+
self.linear2 = None
|
| 546 |
+
if weights_per_step:
|
| 547 |
+
if isinstance(dim_feedforward, int):
|
| 548 |
+
dim_feedforward = [dim_feedforward] * weights_per_step
|
| 549 |
+
assert isinstance(dim_feedforward, list), dim_feedforward
|
| 550 |
+
self.gating = nn.ModuleList(
|
| 551 |
+
[
|
| 552 |
+
make_gating(gating, d_model, dim, **factory_kwargs)
|
| 553 |
+
for dim in dim_feedforward
|
| 554 |
+
]
|
| 555 |
+
)
|
| 556 |
+
else:
|
| 557 |
+
assert isinstance(dim_feedforward, int)
|
| 558 |
+
self.gating = make_gating(
|
| 559 |
+
gating, d_model, dim_feedforward, **factory_kwargs
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
self.layer_scale_1: nn.Module
|
| 563 |
+
self.layer_scale_2: nn.Module
|
| 564 |
+
if layer_scale is None:
|
| 565 |
+
self.layer_scale_1 = nn.Identity()
|
| 566 |
+
self.layer_scale_2 = nn.Identity()
|
| 567 |
+
else:
|
| 568 |
+
self.layer_scale_1 = LayerScale(d_model, layer_scale, **factory_kwargs) # type: ignore
|
| 569 |
+
self.layer_scale_2 = LayerScale(d_model, layer_scale, **factory_kwargs) # type: ignore
|
| 570 |
+
|
| 571 |
+
def _init_streaming_state(self, batch_size: int) -> _LayerState:
|
| 572 |
+
return _LayerState(offset_cpu=0)
|
| 573 |
+
|
| 574 |
+
# feed forward block
|
| 575 |
+
def _ff_block(self, x: torch.Tensor) -> torch.Tensor:
|
| 576 |
+
state = self._streaming_state
|
| 577 |
+
offset = 0
|
| 578 |
+
if state is not None:
|
| 579 |
+
offset = state.offset_cpu
|
| 580 |
+
x_orig = x
|
| 581 |
+
x = self.norm2(x)
|
| 582 |
+
if self.gating is None:
|
| 583 |
+
assert self.linear1 is not None
|
| 584 |
+
assert self.linear2 is not None
|
| 585 |
+
update = self.linear2(self.activation(self.linear1(x)))
|
| 586 |
+
else:
|
| 587 |
+
if self.weights_per_step:
|
| 588 |
+
assert isinstance(self.gating, nn.ModuleList)
|
| 589 |
+
B, T, D = x.shape
|
| 590 |
+
ys = []
|
| 591 |
+
for t in range(T):
|
| 592 |
+
y = self.gating[offset + t](x[:, t : t + 1])
|
| 593 |
+
ys.append(y)
|
| 594 |
+
update = torch.cat(ys, dim=1)
|
| 595 |
+
else:
|
| 596 |
+
update = self.gating(x)
|
| 597 |
+
return x_orig + self.layer_scale_2(update)
|
| 598 |
+
|
| 599 |
+
def _sa_block(self, x: torch.Tensor):
|
| 600 |
+
if self.skip_self_attn:
|
| 601 |
+
return x
|
| 602 |
+
x_orig = x
|
| 603 |
+
x = self.norm1(x)
|
| 604 |
+
update = self.self_attn(x, x, x)
|
| 605 |
+
return x_orig + self.layer_scale_1(update)
|
| 606 |
+
|
| 607 |
+
def forward(self, x: torch.Tensor):
|
| 608 |
+
with ExitStack() as stack:
|
| 609 |
+
if x.device.type != 'cuda':
|
| 610 |
+
stack.enter_context(no_compile())
|
| 611 |
+
x = self._sa_block(x)
|
| 612 |
+
x = self._ff_block(x)
|
| 613 |
+
state = self._streaming_state
|
| 614 |
+
if state:
|
| 615 |
+
state.offset_cpu += x.shape[1]
|
| 616 |
+
return x
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
@dataclass
|
| 620 |
+
class _TransformerState:
|
| 621 |
+
offset: torch.Tensor
|
| 622 |
+
|
| 623 |
+
def reset(self):
|
| 624 |
+
self.offset.zero_()
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
class StreamingTransformer(StreamingModule[_TransformerState]):
|
| 628 |
+
"""Transformer with Streaming / Causal support.
|
| 629 |
+
|
| 630 |
+
Args:
|
| 631 |
+
d_model (int): Dimension of the data.
|
| 632 |
+
num_heads (int): Number of heads.
|
| 633 |
+
dim_feedforward (int): Intermediate dimension of FF module.
|
| 634 |
+
causal (bool): Causal mask applied automatically.
|
| 635 |
+
context (int, optional): Receptive field for the causal mask, infinite if None.
|
| 636 |
+
layer_scale (float, optional): If not None, LayerScale will be used
|
| 637 |
+
with the given value as initial scale.
|
| 638 |
+
positional_embedding (str): Positional embedding strategy (sin, rope, sin_rope, or none).
|
| 639 |
+
max_period (float): Maximum period of the time embedding.
|
| 640 |
+
positional_scale (float): Scale of positional embedding, set to 0 to deactivate.
|
| 641 |
+
layer_class: (subclass of `StreamingTransformerLayer): class to use
|
| 642 |
+
to initialize the layers, allowing further customization outside of AudioCraft.
|
| 643 |
+
device (torch.device, optional): Device on which to initialize.
|
| 644 |
+
dtype (torch.dtype, optional): dtype to use.
|
| 645 |
+
**kwargs: See `StreamingTransformerLayer`.
|
| 646 |
+
"""
|
| 647 |
+
|
| 648 |
+
def __init__(
|
| 649 |
+
self,
|
| 650 |
+
d_model: int,
|
| 651 |
+
num_heads: int,
|
| 652 |
+
num_layers: int,
|
| 653 |
+
dim_feedforward: int | list[int] = 2048,
|
| 654 |
+
causal: bool = False,
|
| 655 |
+
context: tp.Optional[int] = None,
|
| 656 |
+
positional_embedding: str = "sin",
|
| 657 |
+
max_period: float = 10_000,
|
| 658 |
+
positional_scale: float = 1.0,
|
| 659 |
+
betas: tp.Optional[tp.Tuple[float, float]] = None,
|
| 660 |
+
layer_class: tp.Type[StreamingTransformerLayer] = StreamingTransformerLayer,
|
| 661 |
+
device=None,
|
| 662 |
+
dtype=None,
|
| 663 |
+
**kwargs,
|
| 664 |
+
):
|
| 665 |
+
super().__init__()
|
| 666 |
+
assert d_model % num_heads == 0
|
| 667 |
+
|
| 668 |
+
self.positional_embedding = positional_embedding
|
| 669 |
+
self.max_period = max_period
|
| 670 |
+
self.positional_scale = positional_scale
|
| 671 |
+
self.betas = betas
|
| 672 |
+
|
| 673 |
+
assert positional_embedding in {"sin", "rope", "sin_rope", "none"}
|
| 674 |
+
self.rope: tp.Optional[RotaryEmbedding] = None
|
| 675 |
+
if self.positional_embedding in {"rope", "sin_rope"}:
|
| 676 |
+
self.rope = RotaryEmbedding(max_period=max_period)
|
| 677 |
+
|
| 678 |
+
self.layers = nn.ModuleList()
|
| 679 |
+
for _ in range(num_layers):
|
| 680 |
+
self.layers.append(
|
| 681 |
+
layer_class(
|
| 682 |
+
d_model=d_model,
|
| 683 |
+
num_heads=num_heads,
|
| 684 |
+
dim_feedforward=dim_feedforward,
|
| 685 |
+
causal=causal,
|
| 686 |
+
context=context,
|
| 687 |
+
rope=self.rope,
|
| 688 |
+
device=device,
|
| 689 |
+
dtype=dtype,
|
| 690 |
+
**kwargs,
|
| 691 |
+
)
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
def _init_streaming_state(self, batch_size: int) -> _TransformerState:
|
| 695 |
+
device = next(self.parameters()).device
|
| 696 |
+
return _TransformerState(offset=torch.zeros(1, device=device, dtype=torch.long))
|
| 697 |
+
|
| 698 |
+
def forward(self, x: torch.Tensor, *args, **kwargs):
|
| 699 |
+
B, T, C = x.shape
|
| 700 |
+
|
| 701 |
+
state = self._streaming_state
|
| 702 |
+
if state is None:
|
| 703 |
+
offset = torch.zeros(1, dtype=torch.long, device=x.device)
|
| 704 |
+
else:
|
| 705 |
+
offset = state.offset
|
| 706 |
+
|
| 707 |
+
if self.positional_embedding in {"sin", "sin_rope"}:
|
| 708 |
+
positions = torch.arange(T, device=x.device).view(1, -1, 1)
|
| 709 |
+
positions = positions + offset.view(-1, 1, 1)
|
| 710 |
+
pos_emb = create_sin_embedding(
|
| 711 |
+
positions, C, max_period=self.max_period, dtype=x.dtype
|
| 712 |
+
)
|
| 713 |
+
x = x + self.positional_scale * pos_emb
|
| 714 |
+
|
| 715 |
+
for layer in self.layers:
|
| 716 |
+
x = layer(x, *args, **kwargs)
|
| 717 |
+
|
| 718 |
+
if state is not None:
|
| 719 |
+
state.offset.add_(T)
|
| 720 |
+
return x
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
class ProjectedTransformer(StreamingContainer):
|
| 724 |
+
"""Transformer with optional projections of the input and output to different dimensions when needed.
|
| 725 |
+
Supports multiple outputs.
|
| 726 |
+
|
| 727 |
+
Args:
|
| 728 |
+
input_dimension (int): dimension of the input.
|
| 729 |
+
output_dimensions (tuple[int]): dimensions of the outputs.
|
| 730 |
+
d_model (int): inner dimension of the Transformer.
|
| 731 |
+
conv_layout (bool): If True, expects `[B, C, T]` shaped tensors, otherwise, `[B, T, C]`.
|
| 732 |
+
Similarly, the output will have the same layout.
|
| 733 |
+
"""
|
| 734 |
+
|
| 735 |
+
def __init__(
|
| 736 |
+
self,
|
| 737 |
+
input_dimension: int,
|
| 738 |
+
output_dimensions: tp.Tuple[int, ...],
|
| 739 |
+
d_model: int,
|
| 740 |
+
*,
|
| 741 |
+
conv_layout: bool = False,
|
| 742 |
+
**kwargs,
|
| 743 |
+
):
|
| 744 |
+
super().__init__()
|
| 745 |
+
self.transformer = StreamingTransformer(d_model=d_model, **kwargs)
|
| 746 |
+
self.input_dimension = input_dimension
|
| 747 |
+
self.output_dimensions = output_dimensions
|
| 748 |
+
self.conv_layout = conv_layout
|
| 749 |
+
self.input_proj = None
|
| 750 |
+
if d_model != input_dimension:
|
| 751 |
+
self.input_proj = nn.Linear(input_dimension, d_model, bias=False)
|
| 752 |
+
|
| 753 |
+
self.output_projs = nn.ModuleList()
|
| 754 |
+
for output_dimension in output_dimensions:
|
| 755 |
+
if d_model == output_dimension:
|
| 756 |
+
self.output_projs.append(nn.Identity())
|
| 757 |
+
else:
|
| 758 |
+
self.output_projs.append(
|
| 759 |
+
nn.Linear(d_model, output_dimension, bias=False)
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
def forward(self, x, *args, **kwargs):
|
| 763 |
+
if self.conv_layout:
|
| 764 |
+
x = x.transpose(1, 2)
|
| 765 |
+
if self.input_proj is not None:
|
| 766 |
+
x = self.input_proj(x)
|
| 767 |
+
z = self.transformer(x, *args, **kwargs)
|
| 768 |
+
ys = []
|
| 769 |
+
for output_proj in self.output_projs:
|
| 770 |
+
y = output_proj(z)
|
| 771 |
+
if self.conv_layout:
|
| 772 |
+
y = y.transpose(1, 2)
|
| 773 |
+
ys.append(y)
|
| 774 |
+
return ys
|
moshi/offline.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 24 |
+
# This source code is licensed under the license found in the
|
| 25 |
+
# LICENSE file in the root directory of this source tree.
|
| 26 |
+
|
| 27 |
+
"""
|
| 28 |
+
Offline inference entrypoint for PersonaPlex that mirrors server.py behavior without a WebSocket server.
|
| 29 |
+
|
| 30 |
+
High-level flow:
|
| 31 |
+
- Load Mimi encoders/decoders, Moshi LM, and tokenizer (same as server.py)
|
| 32 |
+
- Warmup to initialize CUDA graphs and streaming state
|
| 33 |
+
- Prompt phase: load system text tokens and a voice prompt WAV (agent side)
|
| 34 |
+
- Streaming-like phase: feed user audio frames from a WAV file into the "input" channels,
|
| 35 |
+
autoregressively sample text + agent audio channels each step, and decode audio frames
|
| 36 |
+
- Concatenate generated frames and write an output WAV matching the input duration
|
| 37 |
+
|
| 38 |
+
This script reuses helpers from lm.py (load_audio, _iterate_audio, encode_from_sphn) to
|
| 39 |
+
keep parity with voice-prompt feeding logic in the server.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
import argparse
|
| 43 |
+
import os
|
| 44 |
+
import tarfile
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
import json
|
| 47 |
+
from typing import Optional, List
|
| 48 |
+
|
| 49 |
+
import numpy as np
|
| 50 |
+
import torch
|
| 51 |
+
import sentencepiece
|
| 52 |
+
import sphn
|
| 53 |
+
from huggingface_hub import hf_hub_download
|
| 54 |
+
|
| 55 |
+
from .client_utils import make_log
|
| 56 |
+
from .models import loaders, LMGen, MimiModel
|
| 57 |
+
from .models.lm import load_audio as lm_load_audio
|
| 58 |
+
from .models.lm import _iterate_audio as lm_iterate_audio
|
| 59 |
+
from .models.lm import encode_from_sphn as lm_encode_from_sphn
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def log(level: str, msg: str):
|
| 63 |
+
print(make_log(level, msg))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def seed_all(seed: int):
|
| 67 |
+
"""Seed torch, CUDA, numpy, and Python RNG for reproducible runs.
|
| 68 |
+
|
| 69 |
+
Matches the seeding strategy in server.py.
|
| 70 |
+
"""
|
| 71 |
+
torch.manual_seed(seed)
|
| 72 |
+
if torch.cuda.is_available():
|
| 73 |
+
torch.cuda.manual_seed(seed)
|
| 74 |
+
torch.cuda.manual_seed_all(seed)
|
| 75 |
+
import random
|
| 76 |
+
import numpy as _np
|
| 77 |
+
random.seed(seed)
|
| 78 |
+
_np.random.seed(seed)
|
| 79 |
+
torch.backends.cudnn.deterministic = False
|
| 80 |
+
torch.backends.cudnn.benchmark = False
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def wrap_with_system_tags(text: str) -> str:
|
| 84 |
+
"""Add system tags as the model expects if they are missing.
|
| 85 |
+
Example: "<system> You enjoy having a good conversation. Have a deep conversation about technology. Your name is Jane. <system>"
|
| 86 |
+
"""
|
| 87 |
+
cleaned = text.strip()
|
| 88 |
+
if cleaned.startswith("<system>") and cleaned.endswith("<system>"):
|
| 89 |
+
return cleaned
|
| 90 |
+
return f"<system> {cleaned} <system>"
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def warmup(mimi: MimiModel, other_mimi: MimiModel, lm_gen: LMGen, device: str, frame_size: int):
|
| 94 |
+
"""Run a short warmup loop to initialize CUDA graphs and streaming state.
|
| 95 |
+
|
| 96 |
+
Replicates the same warmup behavior as server.py: zeros → encode → LMGen.step → decode.
|
| 97 |
+
"""
|
| 98 |
+
for _ in range(4):
|
| 99 |
+
chunk = torch.zeros(1, 1, frame_size, dtype=torch.float32, device=device)
|
| 100 |
+
codes = mimi.encode(chunk)
|
| 101 |
+
_ = other_mimi.encode(chunk)
|
| 102 |
+
for c in range(codes.shape[-1]):
|
| 103 |
+
tokens = lm_gen.step(codes[:, :, c : c + 1])
|
| 104 |
+
if tokens is None:
|
| 105 |
+
continue
|
| 106 |
+
# Decode agent audio channels to ensure decode graphs/states are primed
|
| 107 |
+
_ = mimi.decode(tokens[:, 1:9])
|
| 108 |
+
_ = other_mimi.decode(tokens[:, 1:9])
|
| 109 |
+
if torch.cuda.is_available():
|
| 110 |
+
torch.cuda.synchronize()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def decode_tokens_to_pcm(mimi: MimiModel, other_mimi: MimiModel, lm_gen: LMGen, tokens: torch.Tensor) -> np.ndarray:
|
| 114 |
+
"""Decode a single step of model tokens to PCM using Mimi.
|
| 115 |
+
|
| 116 |
+
tokens is shaped [B, dep_q+1, 1]; channels 1..dep_q are the agent audio codebooks.
|
| 117 |
+
Returns a 1D float32 numpy array (mono) for the current frame.
|
| 118 |
+
"""
|
| 119 |
+
pcm = mimi.decode(tokens[:, 1:9])
|
| 120 |
+
_ = other_mimi.decode(tokens[:, 1:9])
|
| 121 |
+
pcm = pcm.detach().cpu().numpy()[0, 0]
|
| 122 |
+
return pcm
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _get_voice_prompt_dir(voice_prompt_dir: Optional[str], hf_repo: str) -> Optional[str]:
|
| 126 |
+
"""
|
| 127 |
+
If voice_prompt_dir is None:
|
| 128 |
+
- download voices.tgz from HF
|
| 129 |
+
- extract it once
|
| 130 |
+
- return extracted directory
|
| 131 |
+
If voice_prompt_dir is provided:
|
| 132 |
+
- just return it
|
| 133 |
+
"""
|
| 134 |
+
if voice_prompt_dir is not None:
|
| 135 |
+
return voice_prompt_dir
|
| 136 |
+
|
| 137 |
+
log("info", "retrieving voice prompts")
|
| 138 |
+
voices_tgz = hf_hub_download(hf_repo, "voices.tgz")
|
| 139 |
+
voices_tgz = Path(voices_tgz)
|
| 140 |
+
voices_dir = voices_tgz.parent / "voices"
|
| 141 |
+
|
| 142 |
+
if not voices_dir.exists():
|
| 143 |
+
log("info", f"extracting {voices_tgz} to {voices_dir}")
|
| 144 |
+
with tarfile.open(voices_tgz, "r:gz") as tar:
|
| 145 |
+
tar.extractall(path=voices_tgz.parent)
|
| 146 |
+
|
| 147 |
+
if not voices_dir.exists():
|
| 148 |
+
raise RuntimeError("voices.tgz did not contain a 'voices/' directory")
|
| 149 |
+
|
| 150 |
+
return str(voices_dir)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def run_inference(
|
| 154 |
+
input_wav: str,
|
| 155 |
+
output_wav: str,
|
| 156 |
+
output_text: str,
|
| 157 |
+
text_prompt: str,
|
| 158 |
+
voice_prompt_path: str,
|
| 159 |
+
tokenizer_path: Optional[str],
|
| 160 |
+
moshi_weight: Optional[str],
|
| 161 |
+
mimi_weight: Optional[str],
|
| 162 |
+
hf_repo: str,
|
| 163 |
+
device: str,
|
| 164 |
+
seed: Optional[int],
|
| 165 |
+
temp_audio: float,
|
| 166 |
+
temp_text: float,
|
| 167 |
+
topk_audio: int,
|
| 168 |
+
topk_text: int,
|
| 169 |
+
greedy: bool,
|
| 170 |
+
save_voice_prompt_embeddings: bool,
|
| 171 |
+
cpu_offload: bool = False,
|
| 172 |
+
):
|
| 173 |
+
"""Run offline inference using an input WAV as the user-side stream.
|
| 174 |
+
|
| 175 |
+
- Loads/initializes models and tokenizer
|
| 176 |
+
- Warms up execution
|
| 177 |
+
- Loads system text tokens and voice prompt
|
| 178 |
+
- Runs prompt phases (text + voice + silences) via LMGen.step_system_prompts
|
| 179 |
+
- Streams the user WAV frames into the input channels and samples model outputs
|
| 180 |
+
- Decodes and writes an output WAV of the same duration
|
| 181 |
+
"""
|
| 182 |
+
if seed is not None and seed != -1:
|
| 183 |
+
seed_all(seed)
|
| 184 |
+
|
| 185 |
+
# Download config.json to increment download counter
|
| 186 |
+
# No worries about double-counting since config.json will be cached the second time
|
| 187 |
+
hf_hub_download(hf_repo, "config.json")
|
| 188 |
+
|
| 189 |
+
# 1) Load Mimi encoders/decoders (same as server.py)
|
| 190 |
+
log("info", "loading mimi")
|
| 191 |
+
if mimi_weight is None:
|
| 192 |
+
mimi_weight = hf_hub_download(hf_repo, loaders.MIMI_NAME) # type: ignore
|
| 193 |
+
mimi = loaders.get_mimi(mimi_weight, device)
|
| 194 |
+
other_mimi = loaders.get_mimi(mimi_weight, device)
|
| 195 |
+
log("info", "mimi loaded")
|
| 196 |
+
|
| 197 |
+
# 2) Load tokenizer
|
| 198 |
+
if tokenizer_path is None:
|
| 199 |
+
tokenizer_path = hf_hub_download(hf_repo, loaders.TEXT_TOKENIZER_NAME) # type: ignore
|
| 200 |
+
text_tokenizer = sentencepiece.SentencePieceProcessor(tokenizer_path) # type: ignore
|
| 201 |
+
|
| 202 |
+
# 3) Load Moshi LM and eval mode
|
| 203 |
+
log("info", "loading moshi")
|
| 204 |
+
if moshi_weight is None:
|
| 205 |
+
moshi_weight = hf_hub_download(hf_repo, loaders.MOSHI_NAME) # type: ignore
|
| 206 |
+
lm = loaders.get_moshi_lm(moshi_weight, device=device, cpu_offload=cpu_offload)
|
| 207 |
+
lm.eval()
|
| 208 |
+
log("info", "moshi loaded")
|
| 209 |
+
|
| 210 |
+
# 4) Construct LMGen like server.py's ServerState does
|
| 211 |
+
frame_size = int(mimi.sample_rate / mimi.frame_rate)
|
| 212 |
+
lm_gen = LMGen(
|
| 213 |
+
lm,
|
| 214 |
+
audio_silence_frame_cnt=int(0.5 * mimi.frame_rate), # spacer after prompts
|
| 215 |
+
sample_rate=mimi.sample_rate,
|
| 216 |
+
device=device,
|
| 217 |
+
frame_rate=mimi.frame_rate,
|
| 218 |
+
save_voice_prompt_embeddings=save_voice_prompt_embeddings,
|
| 219 |
+
use_sampling=not greedy,
|
| 220 |
+
temp=temp_audio,
|
| 221 |
+
temp_text=temp_text,
|
| 222 |
+
top_k=topk_audio,
|
| 223 |
+
top_k_text=topk_text,
|
| 224 |
+
)
|
| 225 |
+
# Keep models in streaming mode similar to the server
|
| 226 |
+
mimi.streaming_forever(1)
|
| 227 |
+
other_mimi.streaming_forever(1)
|
| 228 |
+
lm_gen.streaming_forever(1)
|
| 229 |
+
|
| 230 |
+
# 5) Warmup
|
| 231 |
+
log("info", "warming up the model")
|
| 232 |
+
warmup(mimi, other_mimi, lm_gen, device, frame_size)
|
| 233 |
+
|
| 234 |
+
# 6) Prompt configuration (text + voice)
|
| 235 |
+
# System text tokens (k=0) and agent voice-prompt audio (k=1..dep_q) are forced
|
| 236 |
+
if voice_prompt_path.endswith('.pt'):
|
| 237 |
+
# Load pre-saved voice prompt embeddings
|
| 238 |
+
lm_gen.load_voice_prompt_embeddings(voice_prompt_path)
|
| 239 |
+
else:
|
| 240 |
+
lm_gen.load_voice_prompt(voice_prompt_path)
|
| 241 |
+
lm_gen.text_prompt_tokens = (
|
| 242 |
+
text_tokenizer.encode(wrap_with_system_tags(text_prompt)) if len(text_prompt) > 0 else None
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# 7) Reset streaming and run initial prompt phases
|
| 246 |
+
# - Voice prompt injection
|
| 247 |
+
# - Audio silence
|
| 248 |
+
# - Text prompt injection
|
| 249 |
+
# - Final audio silence
|
| 250 |
+
mimi.reset_streaming()
|
| 251 |
+
other_mimi.reset_streaming()
|
| 252 |
+
lm_gen.reset_streaming()
|
| 253 |
+
lm_gen.step_system_prompts(mimi)
|
| 254 |
+
# Reset mimi streaming after voice prompt encoding
|
| 255 |
+
mimi.reset_streaming()
|
| 256 |
+
|
| 257 |
+
# 8) Load and iterate user audio frames for feeding into the input channels
|
| 258 |
+
sample_rate = mimi.sample_rate
|
| 259 |
+
user_audio = lm_load_audio(input_wav, sample_rate) # (C, T) at model SR
|
| 260 |
+
|
| 261 |
+
# 9) Encode user audio with Mimi (same iterator logic used for voice prompts),
|
| 262 |
+
# and step the model one frame at a time, collecting decoded PCM frames
|
| 263 |
+
generated_frames: List[np.ndarray] = []
|
| 264 |
+
generated_text_tokens: List[str] = []
|
| 265 |
+
total_target_samples = user_audio.shape[-1]
|
| 266 |
+
|
| 267 |
+
for user_encoded in lm_encode_from_sphn(
|
| 268 |
+
mimi,
|
| 269 |
+
lm_iterate_audio(
|
| 270 |
+
user_audio, sample_interval_size=lm_gen._frame_size, pad=True
|
| 271 |
+
),
|
| 272 |
+
max_batch=1,
|
| 273 |
+
):
|
| 274 |
+
# user_encoded: [1, K, T]. Feed one step at a time (usually T==1)
|
| 275 |
+
steps = user_encoded.shape[-1]
|
| 276 |
+
for c in range(steps):
|
| 277 |
+
step_in = user_encoded[:, :, c : c + 1]
|
| 278 |
+
# Feed user-side input channels; text + agent audio are sampled
|
| 279 |
+
tokens = lm_gen.step(step_in)
|
| 280 |
+
if tokens is None:
|
| 281 |
+
continue
|
| 282 |
+
# Decode current sampled agent frame to PCM
|
| 283 |
+
pcm = decode_tokens_to_pcm(mimi, other_mimi, lm_gen, tokens)
|
| 284 |
+
generated_frames.append(pcm)
|
| 285 |
+
# Decode text token
|
| 286 |
+
text_token = tokens[0, 0, 0].item()
|
| 287 |
+
if text_token not in (0, 3):
|
| 288 |
+
_text = text_tokenizer.id_to_piece(text_token) # type: ignore
|
| 289 |
+
_text = _text.replace("▁", " ")
|
| 290 |
+
log("info", f"text token '{_text}'")
|
| 291 |
+
generated_text_tokens.append(_text)
|
| 292 |
+
else:
|
| 293 |
+
text_token_map = ['EPAD', 'BOS', 'EOS', 'PAD']
|
| 294 |
+
log("info", f"text token '{text_token_map[text_token]}'")
|
| 295 |
+
generated_text_tokens.append(text_token_map[text_token])
|
| 296 |
+
|
| 297 |
+
if len(generated_frames) == 0:
|
| 298 |
+
log("error", "No audio frames were generated. Check input file and configuration.")
|
| 299 |
+
return
|
| 300 |
+
|
| 301 |
+
# 10) Concatenate frames and trim/pad to match input duration
|
| 302 |
+
output_pcm = np.concatenate(generated_frames, axis=-1)
|
| 303 |
+
if output_pcm.shape[-1] > total_target_samples:
|
| 304 |
+
output_pcm = output_pcm[:total_target_samples]
|
| 305 |
+
elif output_pcm.shape[-1] < total_target_samples:
|
| 306 |
+
pad_len = total_target_samples - output_pcm.shape[-1]
|
| 307 |
+
output_pcm = np.concatenate(
|
| 308 |
+
[output_pcm, np.zeros(pad_len, dtype=output_pcm.dtype)], axis=-1
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# 11) Write mono WAV at model sample rate
|
| 312 |
+
sphn.write_wav(output_wav, output_pcm, sample_rate)
|
| 313 |
+
log("info", f"Wrote output audio to {output_wav}")
|
| 314 |
+
|
| 315 |
+
# 12) Write text tokens
|
| 316 |
+
with open(output_text, "w") as file:
|
| 317 |
+
json.dump(generated_text_tokens, file, ensure_ascii=False)
|
| 318 |
+
log("info", f"Wrote output text to {output_text}")
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def main():
|
| 322 |
+
"""Parse CLI args and run offline inference."""
|
| 323 |
+
parser = argparse.ArgumentParser(
|
| 324 |
+
description="Offline inference from WAV input using Moshi server components."
|
| 325 |
+
)
|
| 326 |
+
parser.add_argument(
|
| 327 |
+
"--input-wav", required=True, type=str, help="Path to input WAV file (user audio)"
|
| 328 |
+
)
|
| 329 |
+
parser.add_argument(
|
| 330 |
+
"--output-wav", required=True, type=str, help="Path to output WAV file of agent audio to write"
|
| 331 |
+
)
|
| 332 |
+
parser.add_argument(
|
| 333 |
+
"--output-text", required=True, type=str, help="Path to output JSON file of agent text to write"
|
| 334 |
+
)
|
| 335 |
+
parser.add_argument("--text-prompt", default="You are a wise and friendly teacher. Answer questions or provide advice in a clear and engaging way.", type=str, help="Text prompt")
|
| 336 |
+
|
| 337 |
+
parser.add_argument(
|
| 338 |
+
"--voice-prompt", required=True, type=str, help="Voice prompt filename (basename) inside --voice-prompt-dir (e.g. 'NATM1.pt')."
|
| 339 |
+
)
|
| 340 |
+
parser.add_argument(
|
| 341 |
+
"--voice-prompt-dir",
|
| 342 |
+
type=str,
|
| 343 |
+
help=(
|
| 344 |
+
"Directory containing voice prompt files. "
|
| 345 |
+
"If omitted, voices.tgz is downloaded from HF and extracted."
|
| 346 |
+
"Voice prompt filenames from -voice-prompt arg will be joined with this directory path."
|
| 347 |
+
)
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
# Model assets
|
| 351 |
+
parser.add_argument("--tokenizer", type=str, help="Path to a local tokenizer file.")
|
| 352 |
+
parser.add_argument("--moshi-weight", type=str, help="Path to a local checkpoint file for Moshi.")
|
| 353 |
+
parser.add_argument("--mimi-weight", type=str, help="Path to a local checkpoint file for Mimi.")
|
| 354 |
+
parser.add_argument(
|
| 355 |
+
"--hf-repo",
|
| 356 |
+
type=str,
|
| 357 |
+
default=loaders.DEFAULT_REPO,
|
| 358 |
+
help="HF repo to look into (defaults to pre-trained model repo)",
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# Runtime / sampling controls (mirror UI semantics)
|
| 362 |
+
parser.add_argument(
|
| 363 |
+
"--temp-audio", type=float, default=0.8, help="Audio sampling temperature (default: 0.8)"
|
| 364 |
+
)
|
| 365 |
+
parser.add_argument(
|
| 366 |
+
"--temp-text", type=float, default=0.7, help="Text sampling temperature (default: 0.7)"
|
| 367 |
+
)
|
| 368 |
+
parser.add_argument(
|
| 369 |
+
"--topk-audio", type=int, default=250, help="Audio top-k sampling (default: 250)"
|
| 370 |
+
)
|
| 371 |
+
parser.add_argument(
|
| 372 |
+
"--topk-text", type=int, default=25, help="Text top-k sampling (default: 25)"
|
| 373 |
+
)
|
| 374 |
+
parser.add_argument(
|
| 375 |
+
"--greedy", action="store_true", help="Disable sampling (greedy decoding)"
|
| 376 |
+
)
|
| 377 |
+
parser.add_argument(
|
| 378 |
+
"--device", type=str, default="cuda", help="Device on which to run, defaults to 'cuda'."
|
| 379 |
+
)
|
| 380 |
+
parser.add_argument("--cpu-offload", action="store_true",
|
| 381 |
+
help="Offload LM model layers to CPU when GPU memory is insufficient. "
|
| 382 |
+
"Requires 'accelerate' package.")
|
| 383 |
+
parser.add_argument("--seed", type=int, default=-1, help="Seed for reproducibility (-1 disables)")
|
| 384 |
+
|
| 385 |
+
args = parser.parse_args()
|
| 386 |
+
|
| 387 |
+
# If --voice-prompt-dir is omitted, voices.tgz is downloaded from HF and extracted.
|
| 388 |
+
voice_prompt_dir = _get_voice_prompt_dir(
|
| 389 |
+
args.voice_prompt_dir,
|
| 390 |
+
args.hf_repo,
|
| 391 |
+
)
|
| 392 |
+
if not os.path.exists(voice_prompt_dir):
|
| 393 |
+
raise FileNotFoundError(f"voice_prompt_dir does not exist: {voice_prompt_dir}")
|
| 394 |
+
log("info", f"voice_prompt_dir = {voice_prompt_dir}")
|
| 395 |
+
|
| 396 |
+
# Join basename with directory (DO NOT mutate args.voice_prompt)
|
| 397 |
+
voice_prompt_path = os.path.join(voice_prompt_dir, args.voice_prompt)
|
| 398 |
+
if not os.path.exists(voice_prompt_path):
|
| 399 |
+
raise FileNotFoundError(
|
| 400 |
+
f"Voice prompt '{args.voice_prompt}' not found in "
|
| 401 |
+
f"'{voice_prompt_dir}' (resolved: {voice_prompt_path})"
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Normalize greedy flag behavior (True if present, False otherwise)
|
| 405 |
+
greedy = bool(args.greedy)
|
| 406 |
+
|
| 407 |
+
with torch.no_grad():
|
| 408 |
+
run_inference(
|
| 409 |
+
input_wav=args.input_wav,
|
| 410 |
+
output_wav=args.output_wav,
|
| 411 |
+
output_text=args.output_text,
|
| 412 |
+
text_prompt=args.text_prompt,
|
| 413 |
+
voice_prompt_path=voice_prompt_path,
|
| 414 |
+
tokenizer_path=args.tokenizer,
|
| 415 |
+
moshi_weight=args.moshi_weight,
|
| 416 |
+
mimi_weight=args.mimi_weight,
|
| 417 |
+
hf_repo=args.hf_repo,
|
| 418 |
+
device=args.device,
|
| 419 |
+
seed=args.seed,
|
| 420 |
+
temp_audio=args.temp_audio,
|
| 421 |
+
temp_text=args.temp_text,
|
| 422 |
+
topk_audio=args.topk_audio,
|
| 423 |
+
topk_text=args.topk_text,
|
| 424 |
+
greedy=greedy,
|
| 425 |
+
save_voice_prompt_embeddings=False,
|
| 426 |
+
cpu_offload=args.cpu_offload,
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
if __name__ == "__main__":
|
| 431 |
+
main()
|
moshi/quantization/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
"""RVQ."""
|
| 11 |
+
# flake8: noqa
|
| 12 |
+
from .vq import ResidualVectorQuantizer, SplitResidualVectorQuantizer
|
| 13 |
+
from .base import BaseQuantizer, DummyQuantizer, QuantizedResult
|
moshi/quantization/base.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
|
| 11 |
+
"""
|
| 12 |
+
Base class for all quantizers.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from dataclasses import dataclass, field
|
| 16 |
+
import typing as tp
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class QuantizedResult:
|
| 24 |
+
x: torch.Tensor
|
| 25 |
+
codes: torch.Tensor
|
| 26 |
+
bandwidth: torch.Tensor # bandwidth in kb/s used, per batch item.
|
| 27 |
+
penalty: tp.Optional[torch.Tensor] = None
|
| 28 |
+
metrics: dict = field(default_factory=dict)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class BaseQuantizer(nn.Module):
|
| 32 |
+
"""Base class for quantizers."""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self._ema_frozen = False
|
| 37 |
+
|
| 38 |
+
def forward(self, x: torch.Tensor, frame_rate: int) -> QuantizedResult:
|
| 39 |
+
"""
|
| 40 |
+
Given input tensor x, returns first the quantized (or approximately quantized)
|
| 41 |
+
representation along with quantized codes, bandwidth, and any penalty term for the loss.
|
| 42 |
+
Finally, this returns a dict of metrics to update logging etc.
|
| 43 |
+
Frame rate must be passed so that the bandwidth is properly computed.
|
| 44 |
+
"""
|
| 45 |
+
raise NotImplementedError()
|
| 46 |
+
|
| 47 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 48 |
+
"""Encode a given input tensor with the specified sample rate at the given bandwidth."""
|
| 49 |
+
raise NotImplementedError()
|
| 50 |
+
|
| 51 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 52 |
+
"""Decode the given codes to the quantized representation."""
|
| 53 |
+
raise NotImplementedError()
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def cardinality(self) -> int:
|
| 57 |
+
"""Cardinality of each codebook."""
|
| 58 |
+
raise NotImplementedError()
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def total_codebooks(self) -> int:
|
| 62 |
+
"""Total number of codebooks."""
|
| 63 |
+
raise NotImplementedError()
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def num_codebooks(self) -> int:
|
| 67 |
+
"""Number of active codebooks."""
|
| 68 |
+
raise NotImplementedError()
|
| 69 |
+
|
| 70 |
+
@property
|
| 71 |
+
def semantic_quantizer(self) -> 'BaseQuantizer':
|
| 72 |
+
"""This returns the quantizer that models the first level of the hierarchy (typically semantic).
|
| 73 |
+
|
| 74 |
+
In this case, it's the quantizer itself.
|
| 75 |
+
"""
|
| 76 |
+
return self
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def acoustic_quantizer(self) -> 'BaseQuantizer':
|
| 80 |
+
"""This returns the quantizer that models the higher levels of the hierarchy (typically acoustic).
|
| 81 |
+
|
| 82 |
+
In this case, it's the quantizer itself.
|
| 83 |
+
"""
|
| 84 |
+
return self
|
| 85 |
+
|
| 86 |
+
def set_num_codebooks(self, n: int) -> None:
|
| 87 |
+
"""Set the number of active codebooks."""
|
| 88 |
+
raise NotImplementedError()
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def ema_frozen(self) -> bool:
|
| 92 |
+
"""Whether to apply ema to the codebooks."""
|
| 93 |
+
return self._ema_frozen
|
| 94 |
+
|
| 95 |
+
def ema_frozen_(self, ema_frozen: bool) -> None:
|
| 96 |
+
"""Set whether ema should be applied to the codebooks."""
|
| 97 |
+
self._ema_frozen = ema_frozen
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class DummyQuantizer(BaseQuantizer):
|
| 101 |
+
"""Fake quantizer that actually does not perform any quantization."""
|
| 102 |
+
|
| 103 |
+
def __init__(
|
| 104 |
+
self,
|
| 105 |
+
dimension: int,
|
| 106 |
+
input_dimension: tp.Optional[int] = None,
|
| 107 |
+
output_dimension: tp.Optional[int] = None,
|
| 108 |
+
):
|
| 109 |
+
super().__init__()
|
| 110 |
+
self.dimension = dimension
|
| 111 |
+
self.input_dimension = input_dimension or dimension
|
| 112 |
+
self.output_dimension = output_dimension or dimension
|
| 113 |
+
self.input_proj: torch.nn.Module
|
| 114 |
+
self.output_proj: torch.nn.Module
|
| 115 |
+
if self.input_dimension == self.dimension:
|
| 116 |
+
self.input_proj = torch.nn.Identity()
|
| 117 |
+
else:
|
| 118 |
+
self.input_proj = torch.nn.Conv1d(
|
| 119 |
+
self.input_dimension, self.dimension, 1, bias=False
|
| 120 |
+
)
|
| 121 |
+
if self.input_dimension == self.dimension:
|
| 122 |
+
self.output_proj = torch.nn.Identity()
|
| 123 |
+
else:
|
| 124 |
+
self.output_proj = torch.nn.Conv1d(
|
| 125 |
+
self.dimension, self.output_dimension, 1, bias=False
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def forward(self, x: torch.Tensor, frame_rate: int):
|
| 129 |
+
q = x.unsqueeze(1)
|
| 130 |
+
x = self.output_proj(self.input_proj(x))
|
| 131 |
+
return QuantizedResult(
|
| 132 |
+
x, q, torch.tensor(q.numel() * 32 * frame_rate / 1000 / len(x)).to(x)
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 136 |
+
"""Encode a given input tensor with the specified sample rate at the given bandwidth.
|
| 137 |
+
In the case of the DummyQuantizer, the codes are actually identical
|
| 138 |
+
to the input and resulting quantized representation as no quantization is done.
|
| 139 |
+
"""
|
| 140 |
+
x = self.input_proj(x)
|
| 141 |
+
return x.unsqueeze(1)
|
| 142 |
+
|
| 143 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 144 |
+
"""Decode the given codes to the quantized representation.
|
| 145 |
+
In the case of the DummyQuantizer, the codes are actually identical
|
| 146 |
+
to the input and resulting quantized representation as no quantization is done.
|
| 147 |
+
"""
|
| 148 |
+
y = codes.squeeze(1)
|
| 149 |
+
return self.output_proj(y)
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def total_codebooks(self):
|
| 153 |
+
"""Total number of codebooks."""
|
| 154 |
+
return 1
|
| 155 |
+
|
| 156 |
+
@property
|
| 157 |
+
def num_codebooks(self):
|
| 158 |
+
"""Total number of codebooks."""
|
| 159 |
+
return self.total_codebooks
|
| 160 |
+
|
| 161 |
+
def set_num_codebooks(self, n: int):
|
| 162 |
+
"""Set the number of active codebooks."""
|
| 163 |
+
raise AttributeError(
|
| 164 |
+
"Cannot override the number of codebooks for the dummy quantizer"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def cardinality(self) -> int:
|
| 169 |
+
"""Cardinality of each codebook."""
|
| 170 |
+
return 1
|
moshi/quantization/core_vq.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
|
| 11 |
+
import typing as tp
|
| 12 |
+
|
| 13 |
+
from einops import rearrange
|
| 14 |
+
import torch
|
| 15 |
+
from torch import nn
|
| 16 |
+
from torch import distributed
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class _CodebookForwardResult(tp.NamedTuple):
|
| 21 |
+
quantized: torch.Tensor
|
| 22 |
+
codes: torch.Tensor
|
| 23 |
+
metrics: tp.Dict[str, torch.Tensor]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class _VQForwardResult(tp.NamedTuple):
|
| 27 |
+
quantized: torch.Tensor
|
| 28 |
+
codes: torch.Tensor
|
| 29 |
+
loss: torch.Tensor
|
| 30 |
+
metrics: tp.Dict[str, torch.Tensor]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _ema_inplace(moving_avg: torch.Tensor, new: torch.Tensor, decay: float) -> None:
|
| 34 |
+
moving_avg.data.mul_(decay).add_(new, alpha=(1 - decay))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _uniform_init(*shape: int) -> torch.Tensor:
|
| 38 |
+
t = torch.empty(shape)
|
| 39 |
+
nn.init.kaiming_uniform_(t)
|
| 40 |
+
return t
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _sample_vectors(samples: torch.Tensor, num: int) -> torch.Tensor:
|
| 44 |
+
num_samples, device = samples.shape[0], samples.device
|
| 45 |
+
|
| 46 |
+
if num_samples >= num:
|
| 47 |
+
indices = torch.randperm(num_samples, device=device)[:num]
|
| 48 |
+
else:
|
| 49 |
+
indices = torch.randint(0, num_samples, (num,), device=device)
|
| 50 |
+
|
| 51 |
+
return samples[indices]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _compute_entropy(usage: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
# Usage is some unnormalized distribution.
|
| 56 |
+
proba = usage / usage.sum()
|
| 57 |
+
p_log_p = torch.where(
|
| 58 |
+
proba == 0, zero_scalar(usage.device), proba * torch.log(proba)
|
| 59 |
+
)
|
| 60 |
+
return -p_log_p.sum()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _is_distributed() -> bool:
|
| 64 |
+
# Checks if we need to use distributed routines.
|
| 65 |
+
return distributed.is_initialized() and distributed.get_world_size() > 1
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def zero_scalar(device) -> torch.Tensor:
|
| 69 |
+
"""Returns a 0. value on the given device without introducing a synchronization point."""
|
| 70 |
+
return torch.zeros([1], device=device)[0]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class EuclideanCodebook(nn.Module):
|
| 74 |
+
"""Codebook with Euclidean distance.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
dim (int): Dimension.
|
| 78 |
+
codebook_size (int): Codebook size.
|
| 79 |
+
decay (float): Decay for exponential moving average over the codebooks.
|
| 80 |
+
epsilon (float): Epsilon value for numerical stability.
|
| 81 |
+
threshold_usage_ratio (float): Defines the threshold for the cluster usage under which a centroid
|
| 82 |
+
is replaced. This is expressed as a fraction of the usage a centroid would get under
|
| 83 |
+
a uniform distribution, so that it doesn't depend on the batch size etc.
|
| 84 |
+
replaced_usage_ratio (float): When replacing a centroid, use this as an initial centroid usage,
|
| 85 |
+
to avoid the centroid getting replaced too quickly.
|
| 86 |
+
check_unused_every (int): Check for unused centroids every `check_unused_every` iterations.
|
| 87 |
+
This is to avoid too many synchronization points.
|
| 88 |
+
|
| 89 |
+
Buffers:
|
| 90 |
+
cluster_usage (torch.Tensor): EMA of the cluster usage per batch, e.g. this will
|
| 91 |
+
be dependent on the batch size etc.
|
| 92 |
+
embedding_sum (torch.Tensor): EMA of the sum of the assigned points to each cluster.
|
| 93 |
+
In particular, this can be normalized by `cluster_usage` to obtain the
|
| 94 |
+
actual cluster centroids.
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
def __init__(
|
| 98 |
+
self,
|
| 99 |
+
dim: int,
|
| 100 |
+
codebook_size: int,
|
| 101 |
+
decay: float = 0.99,
|
| 102 |
+
epsilon: float = 1e-5,
|
| 103 |
+
threshold_usage_ratio: float = 0.1,
|
| 104 |
+
replaced_usage_ratio: float = 1.0,
|
| 105 |
+
check_unused_every: int = 5,
|
| 106 |
+
):
|
| 107 |
+
super().__init__()
|
| 108 |
+
self.decay = decay
|
| 109 |
+
embedding = torch.zeros(codebook_size, dim)
|
| 110 |
+
|
| 111 |
+
self.dim = dim
|
| 112 |
+
self.codebook_size = codebook_size
|
| 113 |
+
|
| 114 |
+
self.epsilon = epsilon
|
| 115 |
+
self.threshold_usage_ratio = threshold_usage_ratio
|
| 116 |
+
self.replaced_usage_ratio = replaced_usage_ratio
|
| 117 |
+
self.check_unused_every = check_unused_every
|
| 118 |
+
self._next_unused_check = check_unused_every
|
| 119 |
+
|
| 120 |
+
self.register_buffer("_initialized", torch.tensor([False], dtype=torch.float))
|
| 121 |
+
self.register_buffer("cluster_usage", torch.ones(codebook_size))
|
| 122 |
+
self.register_buffer("embedding_sum", embedding)
|
| 123 |
+
self.register_buffer("_embedding", None, persistent=False)
|
| 124 |
+
self._cached_initialized = False
|
| 125 |
+
|
| 126 |
+
def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs) -> None:
|
| 127 |
+
# Mapping old names to new names
|
| 128 |
+
mappings = {
|
| 129 |
+
"inited": "_initialized",
|
| 130 |
+
"cluster_size": "cluster_usage",
|
| 131 |
+
"embed_avg": "embedding_sum",
|
| 132 |
+
"embed_sum": "embedding_sum",
|
| 133 |
+
}
|
| 134 |
+
for old_name, new_name in mappings.items():
|
| 135 |
+
old_name = prefix + old_name
|
| 136 |
+
if old_name in state_dict:
|
| 137 |
+
value = state_dict.pop(old_name)
|
| 138 |
+
if new_name is not None:
|
| 139 |
+
state_dict[prefix + new_name] = value
|
| 140 |
+
super()._load_from_state_dict(state_dict, prefix, *args, **kwargs)
|
| 141 |
+
|
| 142 |
+
@property
|
| 143 |
+
def embedding(self) -> torch.Tensor:
|
| 144 |
+
if self._embedding is None:
|
| 145 |
+
embedding = (
|
| 146 |
+
self.embedding_sum / self.cluster_usage.clamp(min=self.epsilon)[:, None]
|
| 147 |
+
)
|
| 148 |
+
self.register_buffer("_embedding", embedding, persistent=False)
|
| 149 |
+
return embedding
|
| 150 |
+
return self._embedding
|
| 151 |
+
|
| 152 |
+
def _broadcast_buffers(self) -> None:
|
| 153 |
+
if _is_distributed():
|
| 154 |
+
for buffer in self.buffers():
|
| 155 |
+
distributed.broadcast(buffer, 0)
|
| 156 |
+
|
| 157 |
+
def _replace_expired_codes(self, samples: torch.Tensor, mask: torch.Tensor) -> None:
|
| 158 |
+
# Replaces expired centroids, as indicated by `mask` (a true value indicate the code needs to be replaced).
|
| 159 |
+
# The new codes are sampled from the batch `samples`.
|
| 160 |
+
new_vectors = _sample_vectors(samples, self.codebook_size)
|
| 161 |
+
replace_cluster_usage = (
|
| 162 |
+
self.replaced_usage_ratio * self.cluster_usage.sum() / self.codebook_size
|
| 163 |
+
)
|
| 164 |
+
self.embedding_sum[:] = torch.where(
|
| 165 |
+
mask[:, None], replace_cluster_usage * new_vectors, self.embedding_sum
|
| 166 |
+
)
|
| 167 |
+
self.cluster_usage[:] = torch.where(
|
| 168 |
+
mask, replace_cluster_usage, self.cluster_usage
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def _reshape_input(self, x: torch.Tensor) -> torch.Tensor:
|
| 172 |
+
# Flattens all the dimensions but the last one, e.g. return a vector of shape `[N, D]`.
|
| 173 |
+
x = rearrange(x, "... d -> (...) d")
|
| 174 |
+
return x
|
| 175 |
+
|
| 176 |
+
def _reshape_codes(self, codes: torch.Tensor, shape: torch.Size) -> torch.Tensor:
|
| 177 |
+
return codes.view(*shape[:-1])
|
| 178 |
+
|
| 179 |
+
def _quantize(self, x: torch.Tensor) -> torch.Tensor:
|
| 180 |
+
# Projects each vector in `x` over the nearest centroid and return its index.
|
| 181 |
+
# `x` should be `[N, D]` with `N` the number of input vectors and `D` the dimension.
|
| 182 |
+
assert x.dim() == 2
|
| 183 |
+
dists = torch.cdist(x[None], self.embedding[None], p=2)[0]
|
| 184 |
+
codes = dists.argmin(dim=-1)
|
| 185 |
+
return codes
|
| 186 |
+
|
| 187 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 188 |
+
"""Given a tensor `x` of shape `[*, D]`, returns a tensor of integer codes of shape `[*]`.
|
| 189 |
+
The codes are defined as the indexes of the centroids nearest to each vector in `x`.
|
| 190 |
+
"""
|
| 191 |
+
assert x.dtype.is_floating_point, f"Input should be floats, got {x.dtype}"
|
| 192 |
+
shape = x.shape
|
| 193 |
+
x = self._reshape_input(x)
|
| 194 |
+
codes = self._quantize(x)
|
| 195 |
+
codes = self._reshape_codes(codes, shape)
|
| 196 |
+
return codes
|
| 197 |
+
|
| 198 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 199 |
+
"""Given a tensor of codes of shape `[*]`, returns a tensor of shape `[*, D]`,
|
| 200 |
+
corresponding to the centroids associated to each code index.
|
| 201 |
+
"""
|
| 202 |
+
assert (
|
| 203 |
+
not codes.dtype.is_floating_point
|
| 204 |
+
), f"Codes should be integers, got {codes.dtype}"
|
| 205 |
+
quantized = F.embedding(codes, self.embedding)
|
| 206 |
+
return quantized
|
| 207 |
+
|
| 208 |
+
def forward(
|
| 209 |
+
self, x: torch.Tensor, initialize: bool = True
|
| 210 |
+
) -> _CodebookForwardResult:
|
| 211 |
+
shape = x.shape
|
| 212 |
+
x = self._reshape_input(x)
|
| 213 |
+
|
| 214 |
+
flat_codes = self._quantize(x)
|
| 215 |
+
codes = self._reshape_codes(flat_codes, shape)
|
| 216 |
+
quantized = self.decode(codes)
|
| 217 |
+
metrics: tp.Dict[str, torch.Tensor] = {}
|
| 218 |
+
|
| 219 |
+
return _CodebookForwardResult(quantized, codes, metrics)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class VectorQuantization(nn.Module):
|
| 223 |
+
"""Vector quantization implementation.
|
| 224 |
+
Currently supports only euclidean distance.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
dim (int): Dimension
|
| 228 |
+
codebook_size (int): Codebook size
|
| 229 |
+
codebook_dim (int): Codebook dimension. If not defined, uses the specified dimension in dim.
|
| 230 |
+
decay (float): Decay for exponential moving average over the codebooks.
|
| 231 |
+
epsilon (float): Epsilon value for numerical stability.
|
| 232 |
+
threshold_usage_ratio (float): Defines the threshold for the cluster usage under which a centroid
|
| 233 |
+
is replaced. This is expressed as a fraction of the usage a centroid would get under
|
| 234 |
+
a uniform distribution, so that it doesn't depend on the batch size etc.
|
| 235 |
+
replaced_usage_ratio (float): When replacing a centroid, use this as an initial centroid usage,
|
| 236 |
+
to avoid the centroid getting replaced too quickly.
|
| 237 |
+
check_unused_every (int): Check for unused centroids every `check_unused_every` iterations.
|
| 238 |
+
This is to avoid too many synchronization points.
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
def __init__(
|
| 242 |
+
self,
|
| 243 |
+
dim: int,
|
| 244 |
+
codebook_size: int,
|
| 245 |
+
codebook_dim: tp.Optional[int] = None,
|
| 246 |
+
decay: float = 0.99,
|
| 247 |
+
epsilon: float = 1e-5,
|
| 248 |
+
threshold_usage_ratio: float = 0.1,
|
| 249 |
+
**kwargs,
|
| 250 |
+
):
|
| 251 |
+
super().__init__()
|
| 252 |
+
if codebook_dim is None:
|
| 253 |
+
codebook_dim = dim
|
| 254 |
+
|
| 255 |
+
requires_projection = codebook_dim != dim
|
| 256 |
+
self.project_in = (
|
| 257 |
+
nn.Linear(dim, codebook_dim) if requires_projection else nn.Identity()
|
| 258 |
+
)
|
| 259 |
+
self.project_out = (
|
| 260 |
+
nn.Linear(codebook_dim, dim) if requires_projection else nn.Identity()
|
| 261 |
+
)
|
| 262 |
+
self.epsilon = epsilon
|
| 263 |
+
self._codebook = EuclideanCodebook(
|
| 264 |
+
dim=codebook_dim,
|
| 265 |
+
codebook_size=codebook_size,
|
| 266 |
+
decay=decay,
|
| 267 |
+
epsilon=epsilon,
|
| 268 |
+
threshold_usage_ratio=threshold_usage_ratio,
|
| 269 |
+
**kwargs,
|
| 270 |
+
)
|
| 271 |
+
self.codebook_size = codebook_size
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def embedding(self):
|
| 275 |
+
return self._codebook.embedding
|
| 276 |
+
|
| 277 |
+
def _rearrange_input(self, x):
|
| 278 |
+
x = rearrange(x, "b d n -> b n d")
|
| 279 |
+
return x
|
| 280 |
+
|
| 281 |
+
def _rearrange_output(self, quantized):
|
| 282 |
+
quantized = rearrange(quantized, "b n d -> b d n")
|
| 283 |
+
return quantized
|
| 284 |
+
|
| 285 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 286 |
+
"""Encodes `x` into discrete integer codes."""
|
| 287 |
+
x = self._rearrange_input(x)
|
| 288 |
+
x = self.project_in(x)
|
| 289 |
+
codes = self._codebook.encode(x)
|
| 290 |
+
return codes
|
| 291 |
+
|
| 292 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 293 |
+
"""Converts integer codes into quantized vectors."""
|
| 294 |
+
quantized = self._codebook.decode(codes)
|
| 295 |
+
quantized = self.project_out(quantized)
|
| 296 |
+
quantized = self._rearrange_output(quantized)
|
| 297 |
+
return quantized
|
| 298 |
+
|
| 299 |
+
def forward(self, x: torch.Tensor, initialize: bool = True) -> _VQForwardResult:
|
| 300 |
+
x = self._rearrange_input(x)
|
| 301 |
+
quantized, codes, metrics = self._codebook(x, initialize=initialize)
|
| 302 |
+
|
| 303 |
+
loss = zero_scalar(x.device)
|
| 304 |
+
|
| 305 |
+
quantized = self.project_out(quantized)
|
| 306 |
+
quantized = self._rearrange_output(quantized)
|
| 307 |
+
|
| 308 |
+
return _VQForwardResult(quantized, codes, loss, metrics)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
class ResidualVectorQuantization(nn.Module):
|
| 312 |
+
"""Residual vector quantization implementation.
|
| 313 |
+
|
| 314 |
+
Follows Algorithm 1. in https://arxiv.org/pdf/2107.03312.pdf
|
| 315 |
+
"""
|
| 316 |
+
|
| 317 |
+
def __init__(self, *, num_quantizers: int, codebook_offset: int, **kwargs):
|
| 318 |
+
super().__init__()
|
| 319 |
+
self.layers = nn.ModuleList(
|
| 320 |
+
[VectorQuantization(**kwargs) for _ in range(num_quantizers)]
|
| 321 |
+
)
|
| 322 |
+
self.codebook_offset = codebook_offset
|
| 323 |
+
|
| 324 |
+
def forward(
|
| 325 |
+
self, x: torch.Tensor, n_q: tp.Optional[int] = None
|
| 326 |
+
) -> _VQForwardResult:
|
| 327 |
+
"""
|
| 328 |
+
Args:
|
| 329 |
+
x (torch.Tensor): input tensor to quantize, of shape `[B, C, T]`.
|
| 330 |
+
n_q (int or None): if provided, number of codebook levels to use in RVQ.
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
quantized_out = zero_scalar(x.device)
|
| 334 |
+
residual = x
|
| 335 |
+
|
| 336 |
+
all_losses = []
|
| 337 |
+
all_codes = []
|
| 338 |
+
all_metrics: tp.Dict[str, torch.Tensor] = {}
|
| 339 |
+
|
| 340 |
+
n_q = n_q or len(self.layers)
|
| 341 |
+
previous_layer_is_initialized = True
|
| 342 |
+
|
| 343 |
+
for i, layer in enumerate(self.layers[:n_q]): # type: ignore
|
| 344 |
+
quantized, codes, loss, metrics = layer(
|
| 345 |
+
residual, initialize=previous_layer_is_initialized
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
quantized = quantized.detach()
|
| 349 |
+
residual = residual - quantized
|
| 350 |
+
quantized_out = quantized_out + quantized
|
| 351 |
+
|
| 352 |
+
all_codes.append(codes)
|
| 353 |
+
all_losses.append(loss)
|
| 354 |
+
|
| 355 |
+
for key, value in metrics.items():
|
| 356 |
+
if key in all_metrics:
|
| 357 |
+
all_metrics[key] += value / n_q
|
| 358 |
+
else:
|
| 359 |
+
all_metrics[key] = value / n_q
|
| 360 |
+
all_metrics[key + f"_{i + self.codebook_offset}"] = value
|
| 361 |
+
|
| 362 |
+
out_losses, out_codes = map(torch.stack, (all_losses, all_codes))
|
| 363 |
+
return _VQForwardResult(quantized_out, out_codes, out_losses, all_metrics)
|
| 364 |
+
|
| 365 |
+
def encode(self, x: torch.Tensor, n_q: tp.Optional[int] = None) -> torch.Tensor:
|
| 366 |
+
"""Encodes `x` into discrete integer codes. If `n_q` is provided, only uses the first `n_q` codebook levels."""
|
| 367 |
+
residual = x
|
| 368 |
+
all_indices = []
|
| 369 |
+
n_q = n_q or len(self.layers)
|
| 370 |
+
for layer in self.layers[:n_q]: # type: ignore
|
| 371 |
+
indices = layer.encode(residual)
|
| 372 |
+
quantized = layer.decode(indices)
|
| 373 |
+
residual = residual - quantized
|
| 374 |
+
all_indices.append(indices)
|
| 375 |
+
out_indices = torch.stack(all_indices)
|
| 376 |
+
return out_indices
|
| 377 |
+
|
| 378 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 379 |
+
"""Converts the integer codes into quantized vectors."""
|
| 380 |
+
quantized = zero_scalar(codes.device)
|
| 381 |
+
for idx, layer_codes in enumerate(codes):
|
| 382 |
+
layer = self.layers[idx]
|
| 383 |
+
quantized = quantized + layer.decode(layer_codes)
|
| 384 |
+
return quantized
|
moshi/quantization/vq.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
import math
|
| 33 |
+
import typing as tp
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
|
| 37 |
+
from .base import BaseQuantizer, QuantizedResult
|
| 38 |
+
from .core_vq import ResidualVectorQuantization
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class ResidualVectorQuantizer(BaseQuantizer):
|
| 42 |
+
"""Residual Vector Quantizer.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
dimension (int): Dimension of the codebooks.
|
| 46 |
+
input_dimension (None or int): dimension of the input, defaults to `dimension` if not provided.
|
| 47 |
+
output_dimension (None or int): dimension of the output, defaults to `dimension` if not provided.
|
| 48 |
+
n_q (int): Number of vector quantizers used.
|
| 49 |
+
q_dropout (bool): Random quantizer drop out at train time.
|
| 50 |
+
no_quantization_rate (float): Gives the probability of applying no quantization at all
|
| 51 |
+
at train time. The RVQ codebooks will still get the input value to learn the proper codebook.
|
| 52 |
+
bins (int): Codebook size.
|
| 53 |
+
decay (float): Decay for exponential moving average over the codebooks.
|
| 54 |
+
threshold_usage_ratio (float): Defines the threshold for the cluster usage under which a centroid
|
| 55 |
+
is replaced. This is expressed as a fraction of the usage a centroid would get under
|
| 56 |
+
a uniform distribution, so that it doesn't depend on the batch size etc.
|
| 57 |
+
replaced_usage_ratio (float): When replacing a centroid, use this as an initial centroid usage,
|
| 58 |
+
to avoid the centroid getting replaced too quickly.
|
| 59 |
+
codebook_offset (int): Offset to use for the codebook indices. This is useful when using multiple quantizers
|
| 60 |
+
such as in SplitResidualVectorQuantizer.
|
| 61 |
+
force_projection (bool): Whether to force input and output projections even when dimension is constant.
|
| 62 |
+
generator_seed (int or None): seed used to initialize the RNG used for no quantization.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
dimension: int = 128,
|
| 68 |
+
input_dimension: tp.Optional[int] = None,
|
| 69 |
+
output_dimension: tp.Optional[int] = None,
|
| 70 |
+
n_q: int = 8,
|
| 71 |
+
q_dropout: bool = False,
|
| 72 |
+
q_first_only_proba: float = 0.0,
|
| 73 |
+
no_quantization_rate: float = 0.0,
|
| 74 |
+
bins: int = 1024,
|
| 75 |
+
decay: float = 0.99,
|
| 76 |
+
threshold_usage_ratio: float = 0.1,
|
| 77 |
+
replaced_usage_ratio: float = 1.0,
|
| 78 |
+
codebook_offset: int = 0,
|
| 79 |
+
force_projection: bool = False,
|
| 80 |
+
generator_seed: tp.Optional[int] = None,
|
| 81 |
+
):
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.max_n_q = n_q
|
| 84 |
+
self.n_q = n_q
|
| 85 |
+
self.q_dropout = q_dropout
|
| 86 |
+
self.no_quantization_rate = no_quantization_rate
|
| 87 |
+
self.q_first_only_proba = q_first_only_proba
|
| 88 |
+
self.dimension = dimension
|
| 89 |
+
self.input_dimension = input_dimension or dimension
|
| 90 |
+
self.output_dimension = output_dimension or dimension
|
| 91 |
+
self.bins = bins
|
| 92 |
+
self.decay = decay
|
| 93 |
+
self.input_proj: torch.nn.Module
|
| 94 |
+
self.output_proj: torch.nn.Module
|
| 95 |
+
self.generator = None
|
| 96 |
+
if generator_seed is not None:
|
| 97 |
+
self.generator = torch.Generator(
|
| 98 |
+
device="cuda" if torch.cuda.is_available() else "cpu"
|
| 99 |
+
)
|
| 100 |
+
self.generator.manual_seed(generator_seed)
|
| 101 |
+
if self.input_dimension == self.dimension and not force_projection:
|
| 102 |
+
self.input_proj = torch.nn.Identity()
|
| 103 |
+
else:
|
| 104 |
+
self.input_proj = torch.nn.Conv1d(
|
| 105 |
+
self.input_dimension, self.dimension, 1, bias=False
|
| 106 |
+
)
|
| 107 |
+
if self.output_dimension == self.dimension and not force_projection:
|
| 108 |
+
self.output_proj = torch.nn.Identity()
|
| 109 |
+
else:
|
| 110 |
+
self.output_proj = torch.nn.Conv1d(
|
| 111 |
+
self.dimension, self.output_dimension, 1, bias=False
|
| 112 |
+
)
|
| 113 |
+
self.vq = ResidualVectorQuantization(
|
| 114 |
+
dim=self.dimension,
|
| 115 |
+
codebook_size=self.bins,
|
| 116 |
+
num_quantizers=self.n_q,
|
| 117 |
+
decay=self.decay,
|
| 118 |
+
threshold_usage_ratio=threshold_usage_ratio,
|
| 119 |
+
replaced_usage_ratio=replaced_usage_ratio,
|
| 120 |
+
codebook_offset=codebook_offset,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
def forward(self, x: torch.Tensor, frame_rate: int):
|
| 124 |
+
"""
|
| 125 |
+
Args:
|
| 126 |
+
x (torch.Tensor): Input tensor of shape [B, C, T] with `C` number of channels.
|
| 127 |
+
frame_rate (int): frame rate of the input (e.g `T = frame_rate * duration`), used to compute
|
| 128 |
+
the bandwidth.
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
QuantizedResult: Quantized result with the following attributes:
|
| 132 |
+
- `x` (torch.Tensor): Quantized tensor of shape [B, C, T].
|
| 133 |
+
- `codes` (torch.Tensor): Quantized codes of shape [B, K, T] with `K` number of codebooks.
|
| 134 |
+
- `bw` (torch.Tensor): Bandwidth of the quantized tensor in kbits per second.
|
| 135 |
+
- `penalty` (torch.Tensor): Commitment loss.
|
| 136 |
+
- `metrics` (dict): RVQ metrics, in particular rate of dead code replacement, and entropy.
|
| 137 |
+
"""
|
| 138 |
+
n_q = self.n_q
|
| 139 |
+
x = self.input_proj(x)
|
| 140 |
+
|
| 141 |
+
bw_per_q = math.log2(self.bins) * frame_rate / 1000
|
| 142 |
+
quantized, codes, commit_loss, metrics = self.vq(x, n_q=n_q)
|
| 143 |
+
B, _, _ = quantized.shape
|
| 144 |
+
quantized = self.output_proj(quantized)
|
| 145 |
+
codes = codes.transpose(0, 1)
|
| 146 |
+
# codes is [B, K, T], with T frames, K nb of codebooks.
|
| 147 |
+
bw = torch.tensor(n_q * bw_per_q).to(x)
|
| 148 |
+
return QuantizedResult(
|
| 149 |
+
quantized, codes, bw, penalty=torch.mean(commit_loss), metrics=metrics
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 153 |
+
"""Encode a given input tensor with the specified frame rate at the given bandwidth.
|
| 154 |
+
The RVQ encode method sets the appropriate number of quantizer to use
|
| 155 |
+
and returns indices for each quantizer.
|
| 156 |
+
"""
|
| 157 |
+
n_q = self.n_q
|
| 158 |
+
if x.shape[-1] == 0:
|
| 159 |
+
return torch.empty((x.shape[0], n_q, 0), device=x.device, dtype=torch.int64)
|
| 160 |
+
|
| 161 |
+
x = self.input_proj(x)
|
| 162 |
+
codes = self.vq.encode(x, n_q=n_q)
|
| 163 |
+
codes = codes.transpose(0, 1)
|
| 164 |
+
# codes is [B, K, T], with T frames, K nb of codebooks.
|
| 165 |
+
return codes
|
| 166 |
+
|
| 167 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 168 |
+
"""Decode the given codes to the quantized representation."""
|
| 169 |
+
# codes is [B, K, T], with T frames, K nb of codebooks, vq.decode expects [K, B, T].
|
| 170 |
+
codes = codes.transpose(0, 1)
|
| 171 |
+
quantized = self.vq.decode(codes)
|
| 172 |
+
quantized = self.output_proj(quantized)
|
| 173 |
+
return quantized
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def total_codebooks(self):
|
| 177 |
+
return self.max_n_q
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def num_codebooks(self):
|
| 181 |
+
return self.n_q
|
| 182 |
+
|
| 183 |
+
def set_num_codebooks(self, n: int):
|
| 184 |
+
assert n >= 0 and n <= self.max_n_q
|
| 185 |
+
self.n_q = n
|
| 186 |
+
|
| 187 |
+
@property
|
| 188 |
+
def cardinality(self) -> int:
|
| 189 |
+
return self.bins
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class SplitResidualVectorQuantizer(BaseQuantizer):
|
| 193 |
+
"""Residual Vector Quantizer with separate projections for the first quantizer and the rest.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
n_q (int): Number of residual vector quantizers used.
|
| 197 |
+
n_semantic_q (int): Number of residual vector quantizers used for the semantic quantizer.
|
| 198 |
+
no_quantization_mode (str): if 'true_skip', when doing no quantization, the input will not go
|
| 199 |
+
through the sub quantizers. If `independent`, independent decisions are taken by
|
| 200 |
+
the semantic and acoustic quantizers. If `same` (the default), the same decision is taken by both.
|
| 201 |
+
**kwargs: Arguments to the constructor of `ResidualVectorQuantizer` that are shared between both.
|
| 202 |
+
"""
|
| 203 |
+
|
| 204 |
+
def __init__(
|
| 205 |
+
self,
|
| 206 |
+
*,
|
| 207 |
+
n_q: int = 8,
|
| 208 |
+
no_quantization_rate: float = 0.0,
|
| 209 |
+
no_quantization_mode: str = "same",
|
| 210 |
+
n_q_semantic: int = 1,
|
| 211 |
+
**kwargs,
|
| 212 |
+
):
|
| 213 |
+
super().__init__()
|
| 214 |
+
assert n_q > n_q_semantic, (
|
| 215 |
+
f"Number of quantizers {n_q} must be larger "
|
| 216 |
+
f"than the number of semantic quantizers {n_q_semantic}."
|
| 217 |
+
)
|
| 218 |
+
self.max_n_q = n_q
|
| 219 |
+
self.n_q_semantic = n_q_semantic
|
| 220 |
+
self.n_q_acoustic = n_q - n_q_semantic
|
| 221 |
+
if no_quantization_mode == "true_skip":
|
| 222 |
+
self.no_quantization_rate = no_quantization_rate
|
| 223 |
+
# Setting to zero for the underlying RVQ.
|
| 224 |
+
no_quantization_rate = 0.0
|
| 225 |
+
else:
|
| 226 |
+
self.no_quantization_rate = 0.0
|
| 227 |
+
if no_quantization_mode == "same":
|
| 228 |
+
kwargs["generator_seed"] = 1234
|
| 229 |
+
kwargs["no_quantization_rate"] = no_quantization_rate
|
| 230 |
+
q_dropout = kwargs.pop("q_dropout", False)
|
| 231 |
+
self.rvq_first = ResidualVectorQuantizer(
|
| 232 |
+
n_q=n_q_semantic, force_projection=True, q_dropout=False, **kwargs
|
| 233 |
+
)
|
| 234 |
+
self.rvq_rest = ResidualVectorQuantizer(
|
| 235 |
+
n_q=n_q - n_q_semantic,
|
| 236 |
+
codebook_offset=1,
|
| 237 |
+
force_projection=True,
|
| 238 |
+
q_dropout=q_dropout,
|
| 239 |
+
**kwargs,
|
| 240 |
+
)
|
| 241 |
+
if no_quantization_mode == "true_skip":
|
| 242 |
+
assert self.rvq_first.input_dimension == self.rvq_first.output_dimension
|
| 243 |
+
assert self.rvq_rest.input_dimension == self.rvq_rest.output_dimension
|
| 244 |
+
|
| 245 |
+
def _renorm_and_add(
|
| 246 |
+
self,
|
| 247 |
+
first_val: torch.Tensor,
|
| 248 |
+
rest_val: torch.Tensor,
|
| 249 |
+
n_q_semantic: int,
|
| 250 |
+
n_q_acoustic: int,
|
| 251 |
+
):
|
| 252 |
+
"""Renormalizes values from `rvq_first` and `rvq_rest` and adds them.
|
| 253 |
+
|
| 254 |
+
This allows correcting statistics that are normalized by the number of quantizers. To renormalize, we use the
|
| 255 |
+
number of quantizers that are actually used, e.g. taking into account quantizer dropout.
|
| 256 |
+
"""
|
| 257 |
+
n_q = n_q_semantic + n_q_acoustic
|
| 258 |
+
renorm_first_val = first_val * n_q_semantic / n_q
|
| 259 |
+
renorm_rest_val = rest_val * n_q_acoustic / n_q
|
| 260 |
+
return renorm_first_val + renorm_rest_val
|
| 261 |
+
|
| 262 |
+
def forward(self, x: torch.Tensor, frame_rate: int):
|
| 263 |
+
"""
|
| 264 |
+
Args:
|
| 265 |
+
x (torch.Tensor): Input tensor of shape [B, C, T] with `C` number of channels.
|
| 266 |
+
frame_rate (int): frame rate of the input (e.g `T = frame_rate * duration`), used to compute
|
| 267 |
+
the bandwidth.
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
QuantizedResult: Quantized result with the following attributes:
|
| 271 |
+
- `x` (torch.Tensor): Quantized tensor of shape [B, C, T].
|
| 272 |
+
- `codes` (torch.Tensor): Quantized codes of shape [B, K, T] with `K` number of codebooks.
|
| 273 |
+
- `bw` (torch.Tensor): Bandwidth of the quantized tensor in kbits per second.
|
| 274 |
+
- `penalty` (torch.Tensor): Commitment loss.
|
| 275 |
+
- `metrics` (dict): RVQ metrics, in particular rate of dead code replacement, and entropy.
|
| 276 |
+
"""
|
| 277 |
+
semantic_result = self.rvq_first(x, frame_rate)
|
| 278 |
+
if self.n_q == self.n_q_semantic:
|
| 279 |
+
return semantic_result
|
| 280 |
+
acoustic_result = self.rvq_rest(x, frame_rate)
|
| 281 |
+
full_quantized_emb = semantic_result.x + acoustic_result.x
|
| 282 |
+
full_quantized_codes = torch.cat(
|
| 283 |
+
[semantic_result.codes, acoustic_result.codes], dim=1
|
| 284 |
+
)
|
| 285 |
+
# This is the actual number of quantizers used, e.g. taking into account quantizer dropout.
|
| 286 |
+
n_q_semantic = semantic_result.codes.shape[1]
|
| 287 |
+
n_q_acoustic = acoustic_result.codes.shape[1]
|
| 288 |
+
full_quantized_bandwidth = semantic_result.bandwidth + acoustic_result.bandwidth
|
| 289 |
+
full_quantized_penalty = self._renorm_and_add(
|
| 290 |
+
semantic_result.penalty, acoustic_result.penalty, n_q_semantic, n_q_acoustic
|
| 291 |
+
)
|
| 292 |
+
full_quantized_metrics = semantic_result.metrics
|
| 293 |
+
for key, value in acoustic_result.metrics.items():
|
| 294 |
+
if key in full_quantized_metrics:
|
| 295 |
+
full_quantized_metrics[key] = self._renorm_and_add(
|
| 296 |
+
full_quantized_metrics[key], value, n_q_semantic, n_q_acoustic
|
| 297 |
+
)
|
| 298 |
+
else:
|
| 299 |
+
full_quantized_metrics[key] = value
|
| 300 |
+
return QuantizedResult(
|
| 301 |
+
full_quantized_emb,
|
| 302 |
+
full_quantized_codes,
|
| 303 |
+
full_quantized_bandwidth,
|
| 304 |
+
penalty=full_quantized_penalty,
|
| 305 |
+
metrics=full_quantized_metrics,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
| 309 |
+
"""Encode a given input tensor with the specified frame rate at the given bandwidth.
|
| 310 |
+
The RVQ encode method sets the appropriate number of quantizer to use
|
| 311 |
+
and returns indices for each quantizer.
|
| 312 |
+
"""
|
| 313 |
+
codes = self.rvq_first.encode(x)
|
| 314 |
+
if self.n_q > self.n_q_semantic:
|
| 315 |
+
acoustic_codes = self.rvq_rest.encode(x)
|
| 316 |
+
codes = torch.cat([codes, acoustic_codes], dim=1)
|
| 317 |
+
# codes is [B, K, T], with T frames, K nb of codebooks.
|
| 318 |
+
return codes
|
| 319 |
+
|
| 320 |
+
def decode(self, codes: torch.Tensor) -> torch.Tensor:
|
| 321 |
+
"""Decode the given codes to the quantized representation."""
|
| 322 |
+
# codes is [B, K, T], with T frames, K nb of codebooks.
|
| 323 |
+
quantized = self.rvq_first.decode(codes[:, : self.n_q_semantic])
|
| 324 |
+
if codes.shape[1] > self.n_q_semantic:
|
| 325 |
+
quantized += self.rvq_rest.decode(codes[:, self.n_q_semantic :])
|
| 326 |
+
return quantized
|
| 327 |
+
|
| 328 |
+
@property
|
| 329 |
+
def total_codebooks(self):
|
| 330 |
+
return self.rvq_first.max_n_q + self.rvq_rest.max_n_q
|
| 331 |
+
|
| 332 |
+
@property
|
| 333 |
+
def num_codebooks(self):
|
| 334 |
+
return self.rvq_first.num_codebooks + self.rvq_rest.num_codebooks
|
| 335 |
+
|
| 336 |
+
@property
|
| 337 |
+
def n_q(self):
|
| 338 |
+
return self.rvq_first.n_q + self.rvq_rest.n_q
|
| 339 |
+
|
| 340 |
+
@property
|
| 341 |
+
def dimension(self):
|
| 342 |
+
return self.rvq_first.dimension
|
| 343 |
+
|
| 344 |
+
@property
|
| 345 |
+
def semantic_quantizer(self) -> ResidualVectorQuantizer:
|
| 346 |
+
"""This returns the quantizer that models the first level of the hierarchy (typically semantic)."""
|
| 347 |
+
return self.rvq_first
|
| 348 |
+
|
| 349 |
+
@property
|
| 350 |
+
def acoustic_quantizer(self) -> ResidualVectorQuantizer:
|
| 351 |
+
"""This returns the quantizer that models the higher levels of the hierarchy (typically acoustic)."""
|
| 352 |
+
return self.rvq_rest
|
| 353 |
+
|
| 354 |
+
def set_num_codebooks(self, n: int):
|
| 355 |
+
assert n >= self.n_q_semantic and n <= self.total_codebooks
|
| 356 |
+
self.rvq_rest.set_num_codebooks(n - self.n_q_semantic)
|
| 357 |
+
|
| 358 |
+
@property
|
| 359 |
+
def cardinality(self) -> int:
|
| 360 |
+
assert self.rvq_rest.cardinality == self.rvq_first.cardinality
|
| 361 |
+
return self.rvq_first.cardinality
|
moshi/server.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 24 |
+
# This source code is licensed under the license found in the
|
| 25 |
+
# LICENSE file in the root directory of this source tree.
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import asyncio
|
| 29 |
+
from dataclasses import dataclass
|
| 30 |
+
import random
|
| 31 |
+
import os
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
import tarfile
|
| 34 |
+
import time
|
| 35 |
+
import secrets
|
| 36 |
+
import sys
|
| 37 |
+
from typing import Literal, Optional
|
| 38 |
+
|
| 39 |
+
import aiohttp
|
| 40 |
+
from aiohttp import web
|
| 41 |
+
from huggingface_hub import hf_hub_download
|
| 42 |
+
import numpy as np
|
| 43 |
+
import sentencepiece
|
| 44 |
+
import sphn
|
| 45 |
+
import torch
|
| 46 |
+
import random
|
| 47 |
+
|
| 48 |
+
from .client_utils import make_log, colorize
|
| 49 |
+
from .models import loaders, MimiModel, LMModel, LMGen
|
| 50 |
+
from .utils.connection import create_ssl_context, get_lan_ip
|
| 51 |
+
from .utils.logging import setup_logger, ColorizedLog
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
logger = setup_logger(__name__)
|
| 55 |
+
DeviceString = Literal["cuda"] | Literal["cpu"] #| Literal["mps"]
|
| 56 |
+
|
| 57 |
+
def torch_auto_device(requested: Optional[DeviceString] = None) -> torch.device:
|
| 58 |
+
"""Return a torch.device based on the requested string or availability."""
|
| 59 |
+
if requested is not None:
|
| 60 |
+
return torch.device(requested)
|
| 61 |
+
if torch.cuda.is_available():
|
| 62 |
+
return torch.device("cuda")
|
| 63 |
+
#elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
| 64 |
+
# return torch.device("mps")
|
| 65 |
+
return torch.device("cpu")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def seed_all(seed):
|
| 69 |
+
torch.manual_seed(seed)
|
| 70 |
+
if torch.cuda.is_available():
|
| 71 |
+
torch.cuda.manual_seed(seed)
|
| 72 |
+
torch.cuda.manual_seed_all(seed) # for multi-GPU setups
|
| 73 |
+
random.seed(seed)
|
| 74 |
+
np.random.seed(seed)
|
| 75 |
+
torch.backends.cudnn.deterministic = False
|
| 76 |
+
torch.backends.cudnn.benchmark = False
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def wrap_with_system_tags(text: str) -> str:
|
| 80 |
+
"""Add system tags as the model expects if they are missing.
|
| 81 |
+
Example: "<system> You enjoy having a good conversation. Have a deep conversation about technology. Your name is Jane. <system>"
|
| 82 |
+
"""
|
| 83 |
+
cleaned = text.strip()
|
| 84 |
+
if cleaned.startswith("<system>") and cleaned.endswith("<system>"):
|
| 85 |
+
return cleaned
|
| 86 |
+
return f"<system> {cleaned} <system>"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@dataclass
|
| 90 |
+
class ServerState:
|
| 91 |
+
mimi: MimiModel
|
| 92 |
+
other_mimi: MimiModel
|
| 93 |
+
text_tokenizer: sentencepiece.SentencePieceProcessor
|
| 94 |
+
lm_gen: LMGen
|
| 95 |
+
lock: asyncio.Lock
|
| 96 |
+
|
| 97 |
+
def __init__(self, mimi: MimiModel, other_mimi: MimiModel, text_tokenizer: sentencepiece.SentencePieceProcessor,
|
| 98 |
+
lm: LMModel, device: str | torch.device, voice_prompt_dir: str | None = None,
|
| 99 |
+
save_voice_prompt_embeddings: bool = False):
|
| 100 |
+
self.mimi = mimi
|
| 101 |
+
self.other_mimi = other_mimi
|
| 102 |
+
self.text_tokenizer = text_tokenizer
|
| 103 |
+
self.device = device
|
| 104 |
+
self.voice_prompt_dir = voice_prompt_dir
|
| 105 |
+
self.frame_size = int(self.mimi.sample_rate / self.mimi.frame_rate)
|
| 106 |
+
self.lm_gen = LMGen(lm,
|
| 107 |
+
audio_silence_frame_cnt=int(0.5 * self.mimi.frame_rate),
|
| 108 |
+
sample_rate=self.mimi.sample_rate,
|
| 109 |
+
device=device,
|
| 110 |
+
frame_rate=self.mimi.frame_rate,
|
| 111 |
+
save_voice_prompt_embeddings=save_voice_prompt_embeddings,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
self.lock = asyncio.Lock()
|
| 115 |
+
self.mimi.streaming_forever(1)
|
| 116 |
+
self.other_mimi.streaming_forever(1)
|
| 117 |
+
self.lm_gen.streaming_forever(1)
|
| 118 |
+
|
| 119 |
+
def warmup(self):
|
| 120 |
+
for _ in range(4):
|
| 121 |
+
chunk = torch.zeros(1, 1, self.frame_size, dtype=torch.float32, device=self.device)
|
| 122 |
+
codes = self.mimi.encode(chunk)
|
| 123 |
+
_ = self.other_mimi.encode(chunk)
|
| 124 |
+
for c in range(codes.shape[-1]):
|
| 125 |
+
tokens = self.lm_gen.step(codes[:, :, c: c + 1])
|
| 126 |
+
if tokens is None:
|
| 127 |
+
continue
|
| 128 |
+
_ = self.mimi.decode(tokens[:, 1:9])
|
| 129 |
+
_ = self.other_mimi.decode(tokens[:, 1:9])
|
| 130 |
+
|
| 131 |
+
if self.device.type == 'cuda':
|
| 132 |
+
torch.cuda.synchronize()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
async def handle_chat(self, request):
|
| 136 |
+
ws = web.WebSocketResponse()
|
| 137 |
+
await ws.prepare(request)
|
| 138 |
+
clog = ColorizedLog.randomize()
|
| 139 |
+
peer = request.remote # IP
|
| 140 |
+
peer_port = request.transport.get_extra_info("peername")[1] # Port
|
| 141 |
+
clog.log("info", f"Incoming connection from {peer}:{peer_port}")
|
| 142 |
+
|
| 143 |
+
# self.lm_gen.temp = float(request.query["audio_temperature"])
|
| 144 |
+
# self.lm_gen.temp_text = float(request.query["text_temperature"])
|
| 145 |
+
# self.lm_gen.top_k_text = max(1, int(request.query["text_topk"]))
|
| 146 |
+
# self.lm_gen.top_k = max(1, int(request.query["audio_topk"]))
|
| 147 |
+
|
| 148 |
+
# Construct full voice prompt path
|
| 149 |
+
requested_voice_prompt_path = None
|
| 150 |
+
voice_prompt_path = None
|
| 151 |
+
if self.voice_prompt_dir is not None:
|
| 152 |
+
voice_prompt_filename = request.query["voice_prompt"]
|
| 153 |
+
requested_voice_prompt_path = None
|
| 154 |
+
if voice_prompt_filename is not None:
|
| 155 |
+
requested_voice_prompt_path = os.path.join(self.voice_prompt_dir, voice_prompt_filename)
|
| 156 |
+
# If the voice prompt file does not exist, find a valid (s0) voiceprompt file in the directory
|
| 157 |
+
if requested_voice_prompt_path is None or not os.path.exists(requested_voice_prompt_path):
|
| 158 |
+
raise FileNotFoundError(
|
| 159 |
+
f"Requested voice prompt '{voice_prompt_filename}' not found in '{self.voice_prompt_dir}'"
|
| 160 |
+
)
|
| 161 |
+
else:
|
| 162 |
+
voice_prompt_path = requested_voice_prompt_path
|
| 163 |
+
|
| 164 |
+
if self.lm_gen.voice_prompt != voice_prompt_path:
|
| 165 |
+
if voice_prompt_path.endswith('.pt'):
|
| 166 |
+
# Load pre-saved voice prompt embeddings
|
| 167 |
+
self.lm_gen.load_voice_prompt_embeddings(voice_prompt_path)
|
| 168 |
+
else:
|
| 169 |
+
self.lm_gen.load_voice_prompt(voice_prompt_path)
|
| 170 |
+
self.lm_gen.text_prompt_tokens = self.text_tokenizer.encode(wrap_with_system_tags(request.query["text_prompt"])) if len(request.query["text_prompt"]) > 0 else None
|
| 171 |
+
seed = int(request["seed"]) if "seed" in request.query else None
|
| 172 |
+
|
| 173 |
+
async def recv_loop():
|
| 174 |
+
nonlocal close
|
| 175 |
+
try:
|
| 176 |
+
async for message in ws:
|
| 177 |
+
if message.type == aiohttp.WSMsgType.ERROR:
|
| 178 |
+
clog.log("error", f"{ws.exception()}")
|
| 179 |
+
break
|
| 180 |
+
elif message.type == aiohttp.WSMsgType.CLOSED:
|
| 181 |
+
break
|
| 182 |
+
elif message.type == aiohttp.WSMsgType.CLOSE:
|
| 183 |
+
break
|
| 184 |
+
elif message.type != aiohttp.WSMsgType.BINARY:
|
| 185 |
+
clog.log("error", f"unexpected message type {message.type}")
|
| 186 |
+
continue
|
| 187 |
+
message = message.data
|
| 188 |
+
if not isinstance(message, bytes):
|
| 189 |
+
clog.log("error", f"unsupported message type {type(message)}")
|
| 190 |
+
continue
|
| 191 |
+
if len(message) == 0:
|
| 192 |
+
clog.log("warning", "empty message")
|
| 193 |
+
continue
|
| 194 |
+
kind = message[0]
|
| 195 |
+
if kind == 1: # audio
|
| 196 |
+
payload = message[1:]
|
| 197 |
+
opus_reader.append_bytes(payload)
|
| 198 |
+
else:
|
| 199 |
+
clog.log("warning", f"unknown message kind {kind}")
|
| 200 |
+
finally:
|
| 201 |
+
close = True
|
| 202 |
+
clog.log("info", "connection closed")
|
| 203 |
+
|
| 204 |
+
async def opus_loop():
|
| 205 |
+
all_pcm_data = None
|
| 206 |
+
|
| 207 |
+
while True:
|
| 208 |
+
if close:
|
| 209 |
+
return
|
| 210 |
+
await asyncio.sleep(0.001)
|
| 211 |
+
pcm = opus_reader.read_pcm()
|
| 212 |
+
if pcm.shape[-1] == 0:
|
| 213 |
+
continue
|
| 214 |
+
if all_pcm_data is None:
|
| 215 |
+
all_pcm_data = pcm
|
| 216 |
+
else:
|
| 217 |
+
all_pcm_data = np.concatenate((all_pcm_data, pcm))
|
| 218 |
+
while all_pcm_data.shape[-1] >= self.frame_size:
|
| 219 |
+
be = time.time()
|
| 220 |
+
chunk = all_pcm_data[: self.frame_size]
|
| 221 |
+
all_pcm_data = all_pcm_data[self.frame_size:]
|
| 222 |
+
chunk = torch.from_numpy(chunk)
|
| 223 |
+
chunk = chunk.to(device=self.device)[None, None]
|
| 224 |
+
codes = self.mimi.encode(chunk)
|
| 225 |
+
_ = self.other_mimi.encode(chunk)
|
| 226 |
+
for c in range(codes.shape[-1]):
|
| 227 |
+
tokens = self.lm_gen.step(codes[:, :, c: c + 1])
|
| 228 |
+
if tokens is None:
|
| 229 |
+
continue
|
| 230 |
+
assert tokens.shape[1] == self.lm_gen.lm_model.dep_q + 1
|
| 231 |
+
main_pcm = self.mimi.decode(tokens[:, 1:9])
|
| 232 |
+
_ = self.other_mimi.decode(tokens[:, 1:9])
|
| 233 |
+
main_pcm = main_pcm.cpu()
|
| 234 |
+
opus_writer.append_pcm(main_pcm[0, 0].numpy())
|
| 235 |
+
text_token = tokens[0, 0, 0].item()
|
| 236 |
+
if text_token not in (0, 3):
|
| 237 |
+
_text = self.text_tokenizer.id_to_piece(text_token) # type: ignore
|
| 238 |
+
_text = _text.replace("▁", " ")
|
| 239 |
+
msg = b"\x02" + bytes(_text, encoding="utf8")
|
| 240 |
+
await ws.send_bytes(msg)
|
| 241 |
+
else:
|
| 242 |
+
text_token_map = ['EPAD', 'BOS', 'EOS', 'PAD']
|
| 243 |
+
|
| 244 |
+
async def send_loop():
|
| 245 |
+
while True:
|
| 246 |
+
if close:
|
| 247 |
+
return
|
| 248 |
+
await asyncio.sleep(0.001)
|
| 249 |
+
msg = opus_writer.read_bytes()
|
| 250 |
+
if len(msg) > 0:
|
| 251 |
+
await ws.send_bytes(b"\x01" + msg)
|
| 252 |
+
|
| 253 |
+
clog.log("info", "accepted connection")
|
| 254 |
+
if len(request.query["text_prompt"]) > 0:
|
| 255 |
+
clog.log("info", f"text prompt: {request.query['text_prompt']}")
|
| 256 |
+
if len(request.query["voice_prompt"]) > 0:
|
| 257 |
+
clog.log("info", f"voice prompt: {voice_prompt_path} (requested: {requested_voice_prompt_path})")
|
| 258 |
+
close = False
|
| 259 |
+
async with self.lock:
|
| 260 |
+
if seed is not None and seed != -1:
|
| 261 |
+
seed_all(seed)
|
| 262 |
+
|
| 263 |
+
opus_writer = sphn.OpusStreamWriter(self.mimi.sample_rate)
|
| 264 |
+
opus_reader = sphn.OpusStreamReader(self.mimi.sample_rate)
|
| 265 |
+
self.mimi.reset_streaming()
|
| 266 |
+
self.other_mimi.reset_streaming()
|
| 267 |
+
self.lm_gen.reset_streaming()
|
| 268 |
+
async def is_alive():
|
| 269 |
+
if close or ws.closed:
|
| 270 |
+
return False
|
| 271 |
+
try:
|
| 272 |
+
# Check for disconnect without waiting too long
|
| 273 |
+
msg = await asyncio.wait_for(ws.receive(), timeout=0.01)
|
| 274 |
+
if msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
| 275 |
+
return False
|
| 276 |
+
except asyncio.TimeoutError:
|
| 277 |
+
# No messages → client probably still alive
|
| 278 |
+
return True
|
| 279 |
+
except aiohttp.ClientConnectionError:
|
| 280 |
+
return False
|
| 281 |
+
return True
|
| 282 |
+
# Reuse mimi for encoding voice prompt and then reset it before conversation starts
|
| 283 |
+
await self.lm_gen.step_system_prompts_async(self.mimi, is_alive=is_alive)
|
| 284 |
+
self.mimi.reset_streaming()
|
| 285 |
+
clog.log("info", "done with system prompts")
|
| 286 |
+
# Send the handshake.
|
| 287 |
+
if await is_alive():
|
| 288 |
+
await ws.send_bytes(b"\x00")
|
| 289 |
+
clog.log("info", "sent handshake bytes")
|
| 290 |
+
# Clean cancellation manager
|
| 291 |
+
tasks = [
|
| 292 |
+
asyncio.create_task(recv_loop()),
|
| 293 |
+
asyncio.create_task(opus_loop()),
|
| 294 |
+
asyncio.create_task(send_loop()),
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
| 298 |
+
# Force-kill remaining tasks
|
| 299 |
+
for task in pending:
|
| 300 |
+
task.cancel()
|
| 301 |
+
try:
|
| 302 |
+
await task
|
| 303 |
+
except asyncio.CancelledError:
|
| 304 |
+
pass
|
| 305 |
+
await ws.close()
|
| 306 |
+
clog.log("info", "session closed")
|
| 307 |
+
# await asyncio.gather(opus_loop(), recv_loop(), send_loop())
|
| 308 |
+
clog.log("info", "done with connection")
|
| 309 |
+
return ws
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _get_voice_prompt_dir(voice_prompt_dir: Optional[str], hf_repo: str) -> Optional[str]:
|
| 313 |
+
"""
|
| 314 |
+
If voice_prompt_dir is None:
|
| 315 |
+
- download voices.tgz from HF
|
| 316 |
+
- extract it once
|
| 317 |
+
- return extracted directory
|
| 318 |
+
If voice_prompt_dir is provided:
|
| 319 |
+
- just return it
|
| 320 |
+
"""
|
| 321 |
+
if voice_prompt_dir is not None:
|
| 322 |
+
return voice_prompt_dir
|
| 323 |
+
|
| 324 |
+
logger.info("retrieving voice prompts")
|
| 325 |
+
|
| 326 |
+
voices_tgz = hf_hub_download(hf_repo, "voices.tgz")
|
| 327 |
+
voices_tgz = Path(voices_tgz)
|
| 328 |
+
voices_dir = voices_tgz.parent / "voices"
|
| 329 |
+
|
| 330 |
+
if not voices_dir.exists():
|
| 331 |
+
logger.info(f"extracting {voices_tgz} to {voices_dir}")
|
| 332 |
+
with tarfile.open(voices_tgz, "r:gz") as tar:
|
| 333 |
+
tar.extractall(path=voices_tgz.parent)
|
| 334 |
+
|
| 335 |
+
if not voices_dir.exists():
|
| 336 |
+
raise RuntimeError("voices.tgz did not contain a 'voices/' directory")
|
| 337 |
+
|
| 338 |
+
return str(voices_dir)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def _get_static_path(static: Optional[str]) -> Optional[str]:
|
| 342 |
+
if static is None:
|
| 343 |
+
logger.info("retrieving the static content")
|
| 344 |
+
dist_tgz = hf_hub_download("nvidia/personaplex-7b-v1", "dist.tgz")
|
| 345 |
+
dist_tgz = Path(dist_tgz)
|
| 346 |
+
dist = dist_tgz.parent / "dist"
|
| 347 |
+
if not dist.exists():
|
| 348 |
+
with tarfile.open(dist_tgz, "r:gz") as tar:
|
| 349 |
+
tar.extractall(path=dist_tgz.parent)
|
| 350 |
+
return str(dist)
|
| 351 |
+
elif static != "none":
|
| 352 |
+
# When set to the "none" string, we don't serve any static content.
|
| 353 |
+
return static
|
| 354 |
+
return None
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def main():
|
| 358 |
+
parser = argparse.ArgumentParser()
|
| 359 |
+
parser.add_argument("--host", default="localhost", type=str)
|
| 360 |
+
parser.add_argument("--port", default=8998, type=int)
|
| 361 |
+
parser.add_argument("--static", type=str)
|
| 362 |
+
parser.add_argument("--gradio-tunnel", action='store_true', help='Activate a gradio tunnel.')
|
| 363 |
+
parser.add_argument("--gradio-tunnel-token",
|
| 364 |
+
help='Provide a custom (secret) token here to keep getting the same URL.')
|
| 365 |
+
|
| 366 |
+
parser.add_argument("--tokenizer", type=str, help="Path to a local tokenizer file.")
|
| 367 |
+
parser.add_argument("--moshi-weight", type=str, help="Path to a local checkpoint file for Moshi.")
|
| 368 |
+
parser.add_argument("--mimi-weight", type=str, help="Path to a local checkpoint file for Mimi.")
|
| 369 |
+
parser.add_argument("--hf-repo", type=str, default=loaders.DEFAULT_REPO,
|
| 370 |
+
help="HF repo to look into, defaults PersonaPlex. "
|
| 371 |
+
"Use this to select a different pre-trained model.")
|
| 372 |
+
parser.add_argument("--device", type=str, default="cuda", help="Device on which to run, defaults to 'cuda'.")
|
| 373 |
+
parser.add_argument("--cpu-offload", action="store_true",
|
| 374 |
+
help="Offload LM model layers to CPU when GPU memory is insufficient. "
|
| 375 |
+
"Requires 'accelerate' package.")
|
| 376 |
+
parser.add_argument(
|
| 377 |
+
"--voice-prompt-dir",
|
| 378 |
+
type=str,
|
| 379 |
+
help=(
|
| 380 |
+
"Directory containing voice prompt files. "
|
| 381 |
+
"If omitted, voices.tgz is downloaded from HF and extracted."
|
| 382 |
+
"Voice prompt filenames from client requests will be joined with this directory path."
|
| 383 |
+
)
|
| 384 |
+
)
|
| 385 |
+
parser.add_argument(
|
| 386 |
+
"--ssl",
|
| 387 |
+
type=str,
|
| 388 |
+
help=(
|
| 389 |
+
"use https instead of http, this flag should point to a directory "
|
| 390 |
+
"that contains valid key.pem and cert.pem files"
|
| 391 |
+
)
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
args = parser.parse_args()
|
| 395 |
+
args.voice_prompt_dir = _get_voice_prompt_dir(
|
| 396 |
+
args.voice_prompt_dir,
|
| 397 |
+
args.hf_repo,
|
| 398 |
+
)
|
| 399 |
+
if args.voice_prompt_dir is not None:
|
| 400 |
+
assert os.path.exists(args.voice_prompt_dir), \
|
| 401 |
+
f"Directory missing: {args.voice_prompt_dir}"
|
| 402 |
+
logger.info(f"voice_prompt_dir = {args.voice_prompt_dir}")
|
| 403 |
+
|
| 404 |
+
static_path: None | str = _get_static_path(args.static)
|
| 405 |
+
assert static_path is None or os.path.exists(static_path), \
|
| 406 |
+
f"Static path does not exist: {static_path}."
|
| 407 |
+
logger.info(f"static_path = {static_path}")
|
| 408 |
+
args.device = torch_auto_device(args.device)
|
| 409 |
+
|
| 410 |
+
seed_all(42424242)
|
| 411 |
+
|
| 412 |
+
setup_tunnel = None
|
| 413 |
+
tunnel_token = ''
|
| 414 |
+
if args.gradio_tunnel:
|
| 415 |
+
try:
|
| 416 |
+
from gradio import networking # type: ignore
|
| 417 |
+
except ImportError:
|
| 418 |
+
logger.error("Cannot find gradio which is required to activate a tunnel. "
|
| 419 |
+
"Please install with `pip install gradio`.")
|
| 420 |
+
sys.exit(1)
|
| 421 |
+
setup_tunnel = networking.setup_tunnel
|
| 422 |
+
if args.gradio_tunnel_token is None:
|
| 423 |
+
tunnel_token = secrets.token_urlsafe(32)
|
| 424 |
+
else:
|
| 425 |
+
tunnel_token = args.gradio_tunnel_token
|
| 426 |
+
|
| 427 |
+
# Download config.json to increment download counter
|
| 428 |
+
# No worries about double-counting since config.json will be cached the second time
|
| 429 |
+
hf_hub_download(args.hf_repo, "config.json")
|
| 430 |
+
|
| 431 |
+
logger.info("loading mimi")
|
| 432 |
+
if args.mimi_weight is None:
|
| 433 |
+
args.mimi_weight = hf_hub_download(args.hf_repo, loaders.MIMI_NAME)
|
| 434 |
+
mimi = loaders.get_mimi(args.mimi_weight, args.device)
|
| 435 |
+
other_mimi = loaders.get_mimi(args.mimi_weight, args.device)
|
| 436 |
+
logger.info("mimi loaded")
|
| 437 |
+
|
| 438 |
+
if args.tokenizer is None:
|
| 439 |
+
args.tokenizer = hf_hub_download(args.hf_repo, loaders.TEXT_TOKENIZER_NAME)
|
| 440 |
+
text_tokenizer = sentencepiece.SentencePieceProcessor(args.tokenizer) # type: ignore
|
| 441 |
+
|
| 442 |
+
logger.info("loading moshi")
|
| 443 |
+
if args.moshi_weight is None:
|
| 444 |
+
args.moshi_weight = hf_hub_download(args.hf_repo, loaders.MOSHI_NAME)
|
| 445 |
+
lm = loaders.get_moshi_lm(args.moshi_weight, device=args.device, cpu_offload=args.cpu_offload)
|
| 446 |
+
lm.eval()
|
| 447 |
+
logger.info("moshi loaded")
|
| 448 |
+
state = ServerState(
|
| 449 |
+
mimi=mimi,
|
| 450 |
+
other_mimi=other_mimi,
|
| 451 |
+
text_tokenizer=text_tokenizer,
|
| 452 |
+
lm=lm,
|
| 453 |
+
device=args.device,
|
| 454 |
+
voice_prompt_dir=args.voice_prompt_dir,
|
| 455 |
+
save_voice_prompt_embeddings=False,
|
| 456 |
+
)
|
| 457 |
+
logger.info("warming up the model")
|
| 458 |
+
state.warmup()
|
| 459 |
+
app = web.Application()
|
| 460 |
+
app.router.add_get("/api/chat", state.handle_chat)
|
| 461 |
+
if static_path is not None:
|
| 462 |
+
async def handle_root(_):
|
| 463 |
+
return web.FileResponse(os.path.join(static_path, "index.html"))
|
| 464 |
+
|
| 465 |
+
logger.info(f"serving static content from {static_path}")
|
| 466 |
+
app.router.add_get("/", handle_root)
|
| 467 |
+
app.router.add_static(
|
| 468 |
+
"/", path=static_path, follow_symlinks=True, name="static"
|
| 469 |
+
)
|
| 470 |
+
protocol = "http"
|
| 471 |
+
ssl_context = None
|
| 472 |
+
if args.ssl is not None:
|
| 473 |
+
ssl_context, protocol = create_ssl_context(args.ssl)
|
| 474 |
+
host_ip = args.host if args.host not in ("0.0.0.0", "::", "localhost") else get_lan_ip()
|
| 475 |
+
logger.info(f"Access the Web UI directly at {protocol}://{host_ip}:{args.port}")
|
| 476 |
+
if setup_tunnel is not None:
|
| 477 |
+
tunnel = setup_tunnel('localhost', args.port, tunnel_token, None)
|
| 478 |
+
logger.info(f"Tunnel started, if executing on a remote GPU, you can use {tunnel}.")
|
| 479 |
+
web.run_app(app, port=args.port, ssl_context=ssl_context)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
with torch.no_grad():
|
| 483 |
+
main()
|
moshi/utils/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
"""Utilities."""
|
moshi/utils/autocast.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 2 |
+
# This source code is licensed under the license found in the
|
| 3 |
+
# LICENSE file in the root directory of this source tree.
|
| 4 |
+
|
| 5 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 6 |
+
# All rights reserved.
|
| 7 |
+
#
|
| 8 |
+
# This source code is licensed under the license found in the
|
| 9 |
+
# LICENSE file in the root directory of this source tree.
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TorchAutocast:
|
| 15 |
+
"""TorchAutocast utility class.
|
| 16 |
+
Allows you to enable and disable autocast. This is specially useful
|
| 17 |
+
when dealing with different architectures and clusters with different
|
| 18 |
+
levels of support.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
enabled (bool): Whether to enable torch.autocast or not.
|
| 22 |
+
args: Additional args for torch.autocast.
|
| 23 |
+
kwargs: Additional kwargs for torch.autocast
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, enabled: bool, *args, **kwargs):
|
| 27 |
+
self.autocast = torch.autocast(*args, **kwargs) if enabled else None
|
| 28 |
+
|
| 29 |
+
def __enter__(self):
|
| 30 |
+
if self.autocast is None:
|
| 31 |
+
return
|
| 32 |
+
try:
|
| 33 |
+
self.autocast.__enter__()
|
| 34 |
+
except RuntimeError:
|
| 35 |
+
device = self.autocast.device
|
| 36 |
+
dtype = self.autocast.fast_dtype
|
| 37 |
+
raise RuntimeError(
|
| 38 |
+
f"There was an error autocasting with dtype={dtype} device={device}\n"
|
| 39 |
+
"If you are on the FAIR Cluster, you might need to use autocast_dtype=float16"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
def __exit__(self, *args, **kwargs):
|
| 43 |
+
if self.autocast is None:
|
| 44 |
+
return
|
| 45 |
+
self.autocast.__exit__(*args, **kwargs)
|
moshi/utils/compile.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
Provides some extra utilities around torch compile, in particular with a way
|
| 28 |
+
to fully deactivate it easily with a context manager.
|
| 29 |
+
Provides a simple activation checkpointing that is compatible with FSDP and torch compile.
|
| 30 |
+
Finally, provides some utilities for CUDA graphing functions.
|
| 31 |
+
"""
|
| 32 |
+
from contextlib import contextmanager
|
| 33 |
+
from functools import wraps
|
| 34 |
+
import inspect
|
| 35 |
+
import os
|
| 36 |
+
import typing as tp
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
from torch import cuda
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
_compile_disabled: bool = False
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@contextmanager
|
| 46 |
+
def no_compile():
|
| 47 |
+
"""Disable torch.compile locally. Now Pytorch 2.4 provides a function to do that."""
|
| 48 |
+
global _compile_disabled
|
| 49 |
+
|
| 50 |
+
prev_disabled = _compile_disabled
|
| 51 |
+
_compile_disabled = True
|
| 52 |
+
try:
|
| 53 |
+
yield
|
| 54 |
+
finally:
|
| 55 |
+
_compile_disabled = prev_disabled
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def torch_compile_lazy(fun):
|
| 59 |
+
"""torch.compile creates a huge pool of processes, even when not using the function at all,
|
| 60 |
+
e.g. with Dora. This can polute stderr when doing CTRL+C. So we do it in a lazy way.
|
| 61 |
+
"""
|
| 62 |
+
if os.environ.get("NO_TORCH_COMPILE"):
|
| 63 |
+
return fun
|
| 64 |
+
fun_compiled = None
|
| 65 |
+
|
| 66 |
+
@wraps(fun)
|
| 67 |
+
def _wrapped(*args, **kwargs):
|
| 68 |
+
nonlocal fun_compiled
|
| 69 |
+
if _compile_disabled:
|
| 70 |
+
return fun(*args, **kwargs)
|
| 71 |
+
if fun_compiled is None:
|
| 72 |
+
fun_compiled = torch.compile(fun)
|
| 73 |
+
return fun_compiled(*args, **kwargs)
|
| 74 |
+
|
| 75 |
+
return _wrapped
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class Checkpoint(torch.autograd.Function):
|
| 79 |
+
@staticmethod
|
| 80 |
+
def forward(ctx, function, *args) -> tp.Any:
|
| 81 |
+
to_save = []
|
| 82 |
+
ctx.others = []
|
| 83 |
+
ctx.function = function
|
| 84 |
+
# Sources will indicate whether the arg in position N is
|
| 85 |
+
# a tensor stored in ctx.save_for_backward, or inside ctx.others.
|
| 86 |
+
ctx.sources = []
|
| 87 |
+
new_args = []
|
| 88 |
+
for arg in args:
|
| 89 |
+
if isinstance(arg, torch.Tensor):
|
| 90 |
+
to_save.append(arg)
|
| 91 |
+
ctx.sources.append("tensor")
|
| 92 |
+
new_args.append(arg.detach())
|
| 93 |
+
else:
|
| 94 |
+
ctx.sources.append("other")
|
| 95 |
+
ctx.others.append(arg)
|
| 96 |
+
new_args.append(arg)
|
| 97 |
+
ctx.save_for_backward(*to_save)
|
| 98 |
+
# During the forward, we just make a pass with no gradient computed.
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
res = function(*new_args)
|
| 101 |
+
return res
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def backward(ctx, *grads) -> tp.Tuple[tp.Optional[torch.Tensor], ...]:
|
| 105 |
+
pseudo_tensors = []
|
| 106 |
+
with torch.set_grad_enabled(True):
|
| 107 |
+
# We create leaf tensors to collect the output gradients.
|
| 108 |
+
# We call them pseudo_tensors because they are pretending to be the input
|
| 109 |
+
# to `function` but are not directly
|
| 110 |
+
for tensor in ctx.saved_tensors:
|
| 111 |
+
pseudo_tensor = tensor.detach()
|
| 112 |
+
pseudo_tensor.requires_grad_(True)
|
| 113 |
+
pseudo_tensors.append(pseudo_tensor)
|
| 114 |
+
pseudo_tensors_copy = list(pseudo_tensors)
|
| 115 |
+
args = []
|
| 116 |
+
for source in ctx.sources:
|
| 117 |
+
if source == "other":
|
| 118 |
+
args.append(ctx.others.pop(0))
|
| 119 |
+
else:
|
| 120 |
+
assert source == "tensor"
|
| 121 |
+
args.append(pseudo_tensors_copy.pop(0))
|
| 122 |
+
res = ctx.function(*args)
|
| 123 |
+
# The second forward with grad computation allows us to connect the input leaf tensors
|
| 124 |
+
# inside pseudo_tensors, to the outputs of the function called.
|
| 125 |
+
if not isinstance(res, tuple):
|
| 126 |
+
res = (res,)
|
| 127 |
+
# Now we just ask Torch to compute the derivative of `res` given the gradient coming from above
|
| 128 |
+
# `grads`. The computed gradient will end up into the `pseudo_tensors` grad attributes.
|
| 129 |
+
torch.autograd.backward(res, grads)
|
| 130 |
+
out: tp.List[tp.Optional[torch.Tensor]] = [None]
|
| 131 |
+
for source in ctx.sources:
|
| 132 |
+
# We still need to output `None` values for non tensor parameters.
|
| 133 |
+
if source == "other":
|
| 134 |
+
out.append(None)
|
| 135 |
+
else:
|
| 136 |
+
assert source == "tensor"
|
| 137 |
+
out.append(pseudo_tensors.pop(0).grad)
|
| 138 |
+
return tuple(out)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def simple_checkpoint(module: torch.nn.Module, *args, **kwargs):
|
| 142 |
+
"""Custom implementation of checkpointing in PyTorch as the builtin implementation is broken
|
| 143 |
+
when using torch compile. Only supports wrapping a `nn.Module` with a forward with no `*args` or `**kwargs`.
|
| 144 |
+
|
| 145 |
+
https://github.com/pytorch/pytorch/issues/97436.
|
| 146 |
+
Should be resolved in nightlies, but it is quite fun and simple to code it ourselves.
|
| 147 |
+
"""
|
| 148 |
+
if hasattr(module, "_fsdp_wrapped_module"):
|
| 149 |
+
module_for_sig = module._fsdp_wrapped_module
|
| 150 |
+
else:
|
| 151 |
+
module_for_sig = module
|
| 152 |
+
sig = inspect.signature(module_for_sig.forward)
|
| 153 |
+
# We first flatten all arguments to use only *args, to make things easier and because
|
| 154 |
+
# torch.autograd.Function has weird support for kwargs.
|
| 155 |
+
bounded = sig.bind(*args, **kwargs)
|
| 156 |
+
new_args = []
|
| 157 |
+
for name, param in sig.parameters.items():
|
| 158 |
+
if param.kind in {
|
| 159 |
+
inspect.Parameter.VAR_POSITIONAL,
|
| 160 |
+
inspect.Parameter.VAR_KEYWORD,
|
| 161 |
+
}:
|
| 162 |
+
raise RuntimeError("simple_checkpoint doesn't support var args.")
|
| 163 |
+
if name not in bounded.arguments:
|
| 164 |
+
break
|
| 165 |
+
new_args.append(bounded.arguments[name])
|
| 166 |
+
return Checkpoint.apply(module, *new_args)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
_in_cuda_graph = False
|
| 170 |
+
_disable_cuda_graph = False
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def in_cuda_graph() -> bool:
|
| 174 |
+
"""Indicate whether we are in a function that is CUDA Graphed (or will be soon)."""
|
| 175 |
+
return _in_cuda_graph
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@contextmanager
|
| 179 |
+
def _set_in_cuda_graph():
|
| 180 |
+
global _in_cuda_graph
|
| 181 |
+
assert not _in_cuda_graph
|
| 182 |
+
_in_cuda_graph = True
|
| 183 |
+
try:
|
| 184 |
+
yield
|
| 185 |
+
finally:
|
| 186 |
+
_in_cuda_graph = False
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _is_cuda_graph_enabled() -> bool:
|
| 190 |
+
if _disable_cuda_graph:
|
| 191 |
+
return False
|
| 192 |
+
no_cuda_graph = os.environ.get("NO_CUDA_GRAPH", "")
|
| 193 |
+
if no_cuda_graph.lower() not in {"0", "no", "n", ""}:
|
| 194 |
+
return False
|
| 195 |
+
return True
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@contextmanager
|
| 199 |
+
def no_cuda_graph():
|
| 200 |
+
"""Deactivate CUDA Graphing for all the calls in this context manager."""
|
| 201 |
+
global _disable_cuda_graph
|
| 202 |
+
old_value = _disable_cuda_graph
|
| 203 |
+
_disable_cuda_graph = True
|
| 204 |
+
try:
|
| 205 |
+
yield
|
| 206 |
+
finally:
|
| 207 |
+
_disable_cuda_graph = old_value
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class CUDAGraphed:
|
| 211 |
+
"""Allow simple CUDA Graphing of a function.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
func: callable, taking any number of arguments. Its tensors arguments should
|
| 215 |
+
be top level args, not nested in structures (tuples, dicts, etc). Keyword
|
| 216 |
+
arguments are NOT supported for simplicity.
|
| 217 |
+
warmup_steps: how many call to make normally before CUDA Graphing. In particular, this
|
| 218 |
+
allows torch.compiled functions to get properly compiled.
|
| 219 |
+
disabled: if True, just call the func directly, useful to quickly deactivate on CPU.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
def __init__(self, func: tp.Callable, warmup_steps: int = 1, disable: bool = False):
|
| 223 |
+
self.func = func
|
| 224 |
+
self.warmup_steps = warmup_steps
|
| 225 |
+
self.disable = disable
|
| 226 |
+
self._graph: cuda.CUDAGraph | None = None
|
| 227 |
+
self._output: tuple | None = None
|
| 228 |
+
self._args: tuple | None = None
|
| 229 |
+
|
| 230 |
+
def reset(self, warmup_steps: int = 0) -> None:
|
| 231 |
+
"""Reset the state, meaning the next call we get CUDA Graphed again. Useful if some
|
| 232 |
+
shapes have changed, or external state (e.g. KVCache) has changed."""
|
| 233 |
+
self.warmup_steps = warmup_steps
|
| 234 |
+
self._graph = None
|
| 235 |
+
self._output = None
|
| 236 |
+
self._args = None
|
| 237 |
+
|
| 238 |
+
def asdict(self):
|
| 239 |
+
return {}
|
| 240 |
+
|
| 241 |
+
def __call__(self, *args, **kwargs) -> tp.Any:
|
| 242 |
+
if kwargs:
|
| 243 |
+
raise RuntimeError("Named arguments not supported for now.")
|
| 244 |
+
if self.disable or not _is_cuda_graph_enabled() or in_cuda_graph():
|
| 245 |
+
return self.func(*args, **kwargs)
|
| 246 |
+
|
| 247 |
+
def _clone_tensors(args: tuple) -> tuple:
|
| 248 |
+
out: list = []
|
| 249 |
+
for arg in args:
|
| 250 |
+
if isinstance(arg, torch.Tensor):
|
| 251 |
+
arg = arg.clone()
|
| 252 |
+
out.append(arg)
|
| 253 |
+
return tuple(out)
|
| 254 |
+
|
| 255 |
+
def _match_values_copy_tensors(args: tuple, target_args: tuple) -> None:
|
| 256 |
+
if len(args) != len(target_args):
|
| 257 |
+
raise ValueError(
|
| 258 |
+
f"Expected {len(target_args)}, but got {args} for CUDA Graphed function."
|
| 259 |
+
)
|
| 260 |
+
for idx, (source, target) in enumerate(zip(args, target_args)):
|
| 261 |
+
if isinstance(target, torch.Tensor):
|
| 262 |
+
if not isinstance(source, torch.Tensor):
|
| 263 |
+
raise ValueError(
|
| 264 |
+
f"Argument #{idx} was a tensor, and is no longer (now {source})."
|
| 265 |
+
)
|
| 266 |
+
if source.shape != target.shape:
|
| 267 |
+
raise ValueError(
|
| 268 |
+
f"Argument #{idx} had shape {target.shape}, but got shae {source.shape}"
|
| 269 |
+
)
|
| 270 |
+
target.copy_(source)
|
| 271 |
+
else:
|
| 272 |
+
if isinstance(source, torch.Tensor):
|
| 273 |
+
raise ValueError(
|
| 274 |
+
f"Argument #{idx} was not a tensor {target}, but is now one."
|
| 275 |
+
)
|
| 276 |
+
if source is not target and source != target:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
f"Argument #{idx} changed value from {target} to {source}."
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
with _set_in_cuda_graph():
|
| 282 |
+
# Prevent any one under us to try and CUDA Graph things.
|
| 283 |
+
if self._graph is None:
|
| 284 |
+
if self.warmup_steps <= 0:
|
| 285 |
+
self._graph = cuda.CUDAGraph()
|
| 286 |
+
# Making a copy just to ensure those are not used else where.
|
| 287 |
+
self._args = _clone_tensors(args)
|
| 288 |
+
with cuda.graph(self._graph):
|
| 289 |
+
self._output = self.func(*self._args)
|
| 290 |
+
# At this point nothing really happened, so we have to make it run for real.
|
| 291 |
+
self._graph.replay()
|
| 292 |
+
return self._output
|
| 293 |
+
else:
|
| 294 |
+
self.warmup_steps -= 1
|
| 295 |
+
return self.func(*args)
|
| 296 |
+
else:
|
| 297 |
+
assert self._args is not None
|
| 298 |
+
assert self._output is not None
|
| 299 |
+
_match_values_copy_tensors(args, self._args)
|
| 300 |
+
self._graph.replay()
|
| 301 |
+
return self._output
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def cuda_graph(func: tp.Callable, warmup_steps: int = 1):
|
| 305 |
+
"""Just calls `CUDAGraphed` on the given function."""
|
| 306 |
+
if not _is_cuda_graph_enabled():
|
| 307 |
+
return func
|
| 308 |
+
return CUDAGraphed(func, warmup_steps)
|
moshi/utils/connection.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
import socket
|
| 23 |
+
import subprocess
|
| 24 |
+
import platform
|
| 25 |
+
from shutil import which
|
| 26 |
+
import urllib.request
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from .logging import setup_logger
|
| 29 |
+
|
| 30 |
+
logger = setup_logger(__name__)
|
| 31 |
+
|
| 32 |
+
def get_lan_ip():
|
| 33 |
+
"""
|
| 34 |
+
get_lan_ip()
|
| 35 |
+
|
| 36 |
+
Get the LAN IP address of the current machine.
|
| 37 |
+
|
| 38 |
+
Returns
|
| 39 |
+
-------
|
| 40 |
+
str
|
| 41 |
+
LAN IP address as a string.
|
| 42 |
+
"""
|
| 43 |
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 44 |
+
try:
|
| 45 |
+
s.connect(("8.8.8.8", 80))
|
| 46 |
+
return s.getsockname()[0]
|
| 47 |
+
finally:
|
| 48 |
+
s.close()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def ensure_mkcert():
|
| 52 |
+
"""
|
| 53 |
+
Ensure mkcert exists; if not, download the correct binary.
|
| 54 |
+
|
| 55 |
+
Returns
|
| 56 |
+
-------
|
| 57 |
+
str | None
|
| 58 |
+
Path to mkcert binary, or None if installation failed.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
# mkcert already installed?
|
| 62 |
+
mkcert_bin_path = which("mkcert")
|
| 63 |
+
if mkcert_bin_path:
|
| 64 |
+
return mkcert_bin_path
|
| 65 |
+
|
| 66 |
+
logger.info("[auto-cert] mkcert not found, attempting to download...")
|
| 67 |
+
|
| 68 |
+
# Determine OS + arch
|
| 69 |
+
system = platform.system().lower() # "linux", "darwin", "windows"
|
| 70 |
+
machine = platform.machine().lower() # "x86_64", "amd64", "arm64", etc.
|
| 71 |
+
|
| 72 |
+
# Normalize architecture
|
| 73 |
+
if machine in ("x86_64", "amd64"):
|
| 74 |
+
arch = "amd64"
|
| 75 |
+
elif machine in ("aarch64", "arm64"):
|
| 76 |
+
arch = "arm64"
|
| 77 |
+
elif machine.startswith("arm"):
|
| 78 |
+
arch = "arm"
|
| 79 |
+
else:
|
| 80 |
+
logger.error(f"[auto-cert] Unsupported architecture: {machine}")
|
| 81 |
+
return None
|
| 82 |
+
|
| 83 |
+
# Determine filename and URL
|
| 84 |
+
if system == "linux":
|
| 85 |
+
filename = f"mkcert-v1.4.4-linux-{arch}"
|
| 86 |
+
elif system == "darwin":
|
| 87 |
+
filename = f"mkcert-v1.4.4-darwin-{arch}"
|
| 88 |
+
elif system == "windows":
|
| 89 |
+
filename = f"mkcert-v1.4.4-windows-{arch}.exe"
|
| 90 |
+
else:
|
| 91 |
+
logger.error(f"[auto-cert] Unsupported OS: {system}")
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
url = f"https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/{filename}"
|
| 95 |
+
dest_dir = Path.home() / ".local" / "bin"
|
| 96 |
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
dest = dest_dir / ("mkcert.exe" if system == "windows" else "mkcert")
|
| 98 |
+
|
| 99 |
+
logger.info(f"[auto-cert] Downloading: {url}")
|
| 100 |
+
try:
|
| 101 |
+
urllib.request.urlretrieve(url, dest)
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error(f"[auto-cert] Failed to download mkcert: {e}")
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
# Make executable if Unix
|
| 107 |
+
if system != "windows":
|
| 108 |
+
dest.chmod(0o755)
|
| 109 |
+
|
| 110 |
+
logger.info(f"[auto-cert] mkcert installed at {dest}")
|
| 111 |
+
|
| 112 |
+
# Verify it works
|
| 113 |
+
try:
|
| 114 |
+
subprocess.check_call([str(dest), "-help"],
|
| 115 |
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 116 |
+
except Exception:
|
| 117 |
+
logger.error("[auto-cert] mkcert failed to run after install.")
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
return str(dest)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _run_command(cmd):
|
| 124 |
+
"""Run command, return True on success."""
|
| 125 |
+
try:
|
| 126 |
+
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 127 |
+
return True
|
| 128 |
+
except subprocess.CalledProcessError:
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def ensure_mkcert_ca(mkcert_bin: str):
|
| 133 |
+
"""
|
| 134 |
+
Install mkcert CA if not installed yet.
|
| 135 |
+
|
| 136 |
+
Parameters
|
| 137 |
+
----------
|
| 138 |
+
mkcert_bin : str
|
| 139 |
+
Path to mkcert binary.
|
| 140 |
+
"""
|
| 141 |
+
_run_command([mkcert_bin, "-install"])
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def create_cert_if_needed(cert_dir: str):
|
| 145 |
+
"""
|
| 146 |
+
Create cert.pem and key.pem using mkcert if they don't already exist.
|
| 147 |
+
Returns (cert_file, key_file) or (None, None) if mkcert unavailable.
|
| 148 |
+
|
| 149 |
+
Parameters
|
| 150 |
+
----------
|
| 151 |
+
cert_dir : str
|
| 152 |
+
Directory to store or find cert.pem and key.pem.
|
| 153 |
+
|
| 154 |
+
Returns
|
| 155 |
+
-------
|
| 156 |
+
pathlib.Path | None
|
| 157 |
+
Path to cert.pem, or None if not created.
|
| 158 |
+
pathlib.Path | None
|
| 159 |
+
Path to key.pem, or None if not created.
|
| 160 |
+
"""
|
| 161 |
+
cert_dir = Path(cert_dir)
|
| 162 |
+
cert_dir.mkdir(parents=True, exist_ok=True)
|
| 163 |
+
|
| 164 |
+
cert_file = cert_dir / "cert.pem"
|
| 165 |
+
key_file = cert_dir / "key.pem"
|
| 166 |
+
|
| 167 |
+
# Already exists → nothing to do
|
| 168 |
+
if cert_file.exists() and key_file.exists():
|
| 169 |
+
return cert_file, key_file
|
| 170 |
+
|
| 171 |
+
mkcert_bin = ensure_mkcert()
|
| 172 |
+
|
| 173 |
+
if not mkcert_bin:
|
| 174 |
+
logger.warning("[auto-cert] mkcert not installed; falling back to HTTP.")
|
| 175 |
+
return None, None
|
| 176 |
+
|
| 177 |
+
logger.info("[auto-cert] mkcert detected. Ensuring local CA installed...")
|
| 178 |
+
ensure_mkcert_ca(mkcert_bin)
|
| 179 |
+
|
| 180 |
+
# Create cert for localhost, loopback, and LAN IP
|
| 181 |
+
lan_ip = get_lan_ip()
|
| 182 |
+
logger.info(f"[auto-cert] Generating certificate for localhost and {lan_ip}...")
|
| 183 |
+
|
| 184 |
+
success = _run_command([
|
| 185 |
+
mkcert_bin,
|
| 186 |
+
"-cert-file", str(cert_file),
|
| 187 |
+
"-key-file", str(key_file),
|
| 188 |
+
"localhost",
|
| 189 |
+
"127.0.0.1",
|
| 190 |
+
"::1",
|
| 191 |
+
lan_ip
|
| 192 |
+
])
|
| 193 |
+
|
| 194 |
+
if not success:
|
| 195 |
+
logger.warning("[auto-cert] mkcert failed. Using HTTP.")
|
| 196 |
+
return None, None
|
| 197 |
+
|
| 198 |
+
logger.info("[auto-cert] Certificate generated.")
|
| 199 |
+
return cert_file, key_file
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def create_ssl_context(cert_dir: str):
|
| 203 |
+
"""
|
| 204 |
+
Main entry: create SSL context if certificates can be created.
|
| 205 |
+
Returns (ssl_context, protocol_str) where protocol_str is "http" or "https".
|
| 206 |
+
|
| 207 |
+
Parameters
|
| 208 |
+
----------
|
| 209 |
+
cert_dir : str
|
| 210 |
+
Directory to store or find cert.pem and key.pem.
|
| 211 |
+
|
| 212 |
+
Returns
|
| 213 |
+
-------
|
| 214 |
+
ssl.SSLContext | None
|
| 215 |
+
SSL context if HTTPS is available, else None.
|
| 216 |
+
str
|
| 217 |
+
"https" if SSL context created, else "http".
|
| 218 |
+
"""
|
| 219 |
+
cert_file, key_file = create_cert_if_needed(cert_dir)
|
| 220 |
+
|
| 221 |
+
if cert_file is None:
|
| 222 |
+
# mkcert missing → use HTTP
|
| 223 |
+
return None, "http"
|
| 224 |
+
|
| 225 |
+
import ssl
|
| 226 |
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
| 227 |
+
ctx.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file))
|
| 228 |
+
return ctx, "https"
|
moshi/utils/logging.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
import logging
|
| 23 |
+
import sys
|
| 24 |
+
import random
|
| 25 |
+
import string
|
| 26 |
+
from typing import Optional
|
| 27 |
+
from ..client_utils import make_log, colorize
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def random_id(n=4):
|
| 31 |
+
return "".join(random.choices(string.ascii_uppercase + string.digits, k=n))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def setup_logger(name: str, log_file=None, level=logging.INFO):
|
| 35 |
+
logger = logging.getLogger(name)
|
| 36 |
+
logger.setLevel(level)
|
| 37 |
+
|
| 38 |
+
formatter = logging.Formatter(
|
| 39 |
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 43 |
+
console_handler.setFormatter(formatter)
|
| 44 |
+
logger.addHandler(console_handler)
|
| 45 |
+
|
| 46 |
+
if log_file:
|
| 47 |
+
file_handler = logging.FileHandler(log_file)
|
| 48 |
+
file_handler.setFormatter(formatter)
|
| 49 |
+
logger.addHandler(file_handler)
|
| 50 |
+
|
| 51 |
+
return logger
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def print_log(level: str, msg: str, prefix: Optional[str] = None, info_color: Optional[str] = None):
|
| 55 |
+
colorized_msg = make_log(level, msg) if info_color is None or level != "info" else colorize(msg, info_color)
|
| 56 |
+
if prefix is None:
|
| 57 |
+
print(colorized_msg)
|
| 58 |
+
else:
|
| 59 |
+
print(prefix + colorized_msg)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class ColorizedLog(object):
|
| 63 |
+
def __init__(self, prefix: str, info_color: str):
|
| 64 |
+
self.prefix = prefix
|
| 65 |
+
self.info_color = info_color
|
| 66 |
+
|
| 67 |
+
def log(self, level: str, msg: str):
|
| 68 |
+
print_log(level, msg, prefix=self.prefix, info_color=self.info_color)
|
| 69 |
+
|
| 70 |
+
@classmethod
|
| 71 |
+
def randomize(cls):
|
| 72 |
+
cid = random_id()
|
| 73 |
+
color = random.choice(["91", "92", "93", "94", "95", "96", "97"])
|
| 74 |
+
prefix = colorize(f"[{cid}] ", color)
|
| 75 |
+
return cls(prefix=prefix, info_color=color)
|
moshi/utils/sampling.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: MIT
|
| 3 |
+
#
|
| 4 |
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
| 5 |
+
# copy of this software and associated documentation files (the "Software"),
|
| 6 |
+
# to deal in the Software without restriction, including without limitation
|
| 7 |
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
| 8 |
+
# and/or sell copies of the Software, and to permit persons to whom the
|
| 9 |
+
# Software is furnished to do so, subject to the following conditions:
|
| 10 |
+
#
|
| 11 |
+
# The above copyright notice and this permission notice shall be included in
|
| 12 |
+
# all copies or substantial portions of the Software.
|
| 13 |
+
#
|
| 14 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
| 17 |
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 19 |
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
| 20 |
+
# DEALINGS IN THE SOFTWARE.
|
| 21 |
+
|
| 22 |
+
# Copyright (c) Kyutai, all rights reserved.
|
| 23 |
+
# This source code is licensed under the license found in the
|
| 24 |
+
# LICENSE file in the root directory of this source tree.
|
| 25 |
+
|
| 26 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 27 |
+
# All rights reserved.
|
| 28 |
+
#
|
| 29 |
+
# This source code is licensed under the license found in the
|
| 30 |
+
# LICENSE file in the root directory of this source tree.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def multinomial(
|
| 37 |
+
input: torch.Tensor, num_samples: int, replacement=False, *, generator=None
|
| 38 |
+
):
|
| 39 |
+
"""torch.multinomial with arbitrary number of dimensions, and number of candidates on the last dimension.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
input (torch.Tensor): The input tensor containing probabilities.
|
| 43 |
+
num_samples (int): Number of samples to draw.
|
| 44 |
+
replacement (bool): Whether to draw with replacement or not.
|
| 45 |
+
Keywords args:
|
| 46 |
+
generator (torch.Generator): A pseudorandom number generator for sampling.
|
| 47 |
+
Returns:
|
| 48 |
+
torch.Tensor: Last dimension contains num_samples indices
|
| 49 |
+
sampled from the multinomial probability distribution
|
| 50 |
+
located in the last dimension of tensor input.
|
| 51 |
+
"""
|
| 52 |
+
input_ = input.reshape(-1, input.shape[-1])
|
| 53 |
+
# We should probably be able to remove this once the following PR has landed:
|
| 54 |
+
# https://github.com/pytorch/pytorch/pull/134818/files
|
| 55 |
+
# In the meantime, we specialize the case no-replacement, nsamples=1 so as not
|
| 56 |
+
# to have a synchronization point.
|
| 57 |
+
if replacement or num_samples != 1:
|
| 58 |
+
output_ = torch.multinomial(
|
| 59 |
+
input_,
|
| 60 |
+
num_samples=num_samples,
|
| 61 |
+
replacement=replacement,
|
| 62 |
+
generator=generator,
|
| 63 |
+
)
|
| 64 |
+
else:
|
| 65 |
+
q = torch.empty_like(input_).exponential_(1, generator=generator)
|
| 66 |
+
q = input_ / q
|
| 67 |
+
output_ = q.argmax(dim=-1, keepdim=True)
|
| 68 |
+
output = output_.reshape(*list(input.shape[:-1]), -1)
|
| 69 |
+
return output
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def sample_top_k(probs: torch.Tensor, k: int) -> torch.Tensor:
|
| 73 |
+
"""Sample next token from top K values along the last dimension of the input probs tensor.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
probs (torch.Tensor): Input probabilities with token candidates on the last dimension.
|
| 77 |
+
k (int): The k in “top-k”.
|
| 78 |
+
Returns:
|
| 79 |
+
torch.Tensor: Sampled tokens.
|
| 80 |
+
"""
|
| 81 |
+
probs, indices = torch.topk(probs, k, dim=-1)
|
| 82 |
+
next_token = multinomial(probs, num_samples=1)
|
| 83 |
+
next_token = indices.gather(-1, next_token)
|
| 84 |
+
return next_token
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def sample_top_p(probs: torch.Tensor, p: float) -> torch.Tensor:
|
| 88 |
+
"""Sample next token from top P probabilities along the last dimension of the input probs tensor.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
probs (torch.Tensor): Input probabilities with token candidates on the last dimension.
|
| 92 |
+
p (int): The p in “top-p”.
|
| 93 |
+
Returns:
|
| 94 |
+
torch.Tensor: Sampled tokens.
|
| 95 |
+
"""
|
| 96 |
+
probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
|
| 97 |
+
probs_sum = torch.cumsum(probs_sort, dim=-1)
|
| 98 |
+
mask = probs_sum - probs_sort > p
|
| 99 |
+
probs_sort *= (~mask).float()
|
| 100 |
+
probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True))
|
| 101 |
+
next_token = multinomial(probs_sort, num_samples=1)
|
| 102 |
+
next_token = torch.gather(probs_idx, -1, next_token)
|
| 103 |
+
return next_token
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def sample_token(
|
| 107 |
+
logits: torch.Tensor,
|
| 108 |
+
use_sampling: bool = False,
|
| 109 |
+
temp: float = 1.0,
|
| 110 |
+
top_k: int = 0,
|
| 111 |
+
top_p: float = 0.0,
|
| 112 |
+
) -> torch.Tensor:
|
| 113 |
+
"""Given logits of shape [*, Card], returns a LongTensor of shape [*]."""
|
| 114 |
+
# Apply softmax for sampling if temp > 0. Else, do greedy sampling to avoid zero division error.
|
| 115 |
+
if use_sampling and temp > 0.0:
|
| 116 |
+
probs = torch.softmax(logits / temp, dim=-1)
|
| 117 |
+
if top_p > 0.0:
|
| 118 |
+
next_token = sample_top_p(probs, p=top_p)
|
| 119 |
+
elif top_k > 0:
|
| 120 |
+
next_token = sample_top_k(probs, k=top_k)
|
| 121 |
+
else:
|
| 122 |
+
next_token = multinomial(probs, num_samples=1)
|
| 123 |
+
else:
|
| 124 |
+
next_token = torch.argmax(logits, dim=-1, keepdim=True)
|
| 125 |
+
assert next_token.shape[-1] == 1
|
| 126 |
+
return next_token[..., 0]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
torch.manual_seed(1234)
|
| 131 |
+
device = "cpu"
|
| 132 |
+
if torch.cuda.is_available():
|
| 133 |
+
torch.backends.cuda.matmul.allow_tf32 = False
|
| 134 |
+
torch.backends.cudnn.allow_tf32 = False
|
| 135 |
+
device = "cuda:0"
|
| 136 |
+
|
| 137 |
+
ps = torch.tensor([5.0, 2.0, 12.0, 6.0, 8.0, 1.0, 0.0, 4.0], device=device)
|
| 138 |
+
cnts = torch.zeros(ps.shape, dtype=torch.long, device=device)
|
| 139 |
+
total_samples = 1000
|
| 140 |
+
for _ in range(total_samples):
|
| 141 |
+
vs = multinomial(ps, num_samples=1, replacement=False)
|
| 142 |
+
cnts[vs] += 1
|
| 143 |
+
diff = cnts / cnts.sum() - ps / ps.sum()
|
| 144 |
+
max_diff = diff.abs().max().cpu().item()
|
| 145 |
+
print(ps / ps.sum())
|
| 146 |
+
print(cnts / cnts.sum())
|
| 147 |
+
assert max_diff < 1.5e-2
|
requirements.txt
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
|
|
| 1 |
gradio==4.44.1
|
| 2 |
fastapi==0.112.4
|
| 3 |
starlette==0.38.6
|
| 4 |
-
torch
|
| 5 |
-
numpy
|
| 6 |
pydantic==2.10.6
|
| 7 |
-
huggingface_hub
|
| 8 |
-
sentencepiece
|
| 9 |
-
sphn
|
| 10 |
-
safetensors
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cu128
|
| 2 |
gradio==4.44.1
|
| 3 |
fastapi==0.112.4
|
| 4 |
starlette==0.38.6
|
| 5 |
+
torch==2.8.0+cu128
|
| 6 |
+
numpy>=1.26,<2.2
|
| 7 |
pydantic==2.10.6
|
| 8 |
+
huggingface_hub>=0.24,<0.25
|
| 9 |
+
sentencepiece==0.2
|
| 10 |
+
sphn>=0.1.4,<0.2
|
| 11 |
+
safetensors>=0.4,<0.5
|
| 12 |
+
einops==0.7
|
| 13 |
+
tqdm
|
| 14 |
+
sounddevice==0.5
|
| 15 |
+
aiohttp>=3.10.5,<3.11
|