AgentNewTwo commited on
Commit
cbc0e90
·
1 Parent(s): 8010b00

Repair PersonaPlex for Blackwell ZeroGPU

Browse files
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
- git+https://github.com/NVIDIA/personaplex.git@3428dfd95309a7f3c84fd93259ded0f810d1ff91#subdirectory=moshi
 
 
 
 
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