muscriptor 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,321 @@
1
+ """Minimal conditioners for muscriptor inference.
2
+
3
+ Contains only the classes needed to run the transcription model:
4
+ - ConditioningAttributes, ConditionType, WavCondition
5
+ - MelSpectrogramConditioner (audio → mel → linear projection)
6
+ - ClassConditioner (class index → embedding)
7
+ - ConditioningProvider
8
+ - nullify_all_conditions (for CFG at inference)
9
+ """
10
+
11
+ import time
12
+ from collections import defaultdict
13
+ from copy import deepcopy
14
+ from dataclasses import dataclass, field
15
+ from typing import NamedTuple, Any
16
+ import torch
17
+ from torch import nn
18
+ from torch.nn import functional as F
19
+ from einops import rearrange
20
+
21
+ from muscriptor.modules.mel_spectrogram import _MelSpectrogram
22
+ from muscriptor.utils.sampling import length_to_mask
23
+
24
+
25
+ ConditionType = tuple[torch.Tensor, torch.Tensor] # (embedding [B, T, D], mask [B, T])
26
+
27
+
28
+ class WavCondition(NamedTuple):
29
+ wav: torch.Tensor
30
+ length: torch.Tensor
31
+ sample_rate: list[int]
32
+ path: list[str | None] = []
33
+ seek_time: list[float | None] = []
34
+
35
+
36
+ @dataclass
37
+ class ConditioningAttributes:
38
+ text: dict[str, str | None] = field(default_factory=dict)
39
+ wav: dict[str, WavCondition] = field(default_factory=dict)
40
+ joint_embed: dict[str, Any] = field(default_factory=dict)
41
+ symbolic: dict[str, Any] = field(default_factory=dict)
42
+
43
+ def __getitem__(self, item):
44
+ return getattr(self, item)
45
+
46
+ @property
47
+ def text_attributes(self):
48
+ return self.text.keys()
49
+
50
+ @property
51
+ def wav_attributes(self):
52
+ return self.wav.keys()
53
+
54
+ @property
55
+ def joint_embed_attributes(self):
56
+ return self.joint_embed.keys()
57
+
58
+ @property
59
+ def symbolic_attributes(self):
60
+ return self.symbolic.keys()
61
+
62
+ @property
63
+ def attributes(self):
64
+ return {
65
+ "text": self.text_attributes,
66
+ "wav": self.wav_attributes,
67
+ "joint_embed": self.joint_embed_attributes,
68
+ "symbolic": self.symbolic_attributes,
69
+ }
70
+
71
+ @classmethod
72
+ def condition_types(cls) -> list:
73
+ return ["text", "wav", "joint_embed", "symbolic"]
74
+
75
+
76
+ def nullify_wav(cond: WavCondition) -> WavCondition:
77
+ B = cond.wav.shape[0]
78
+ return WavCondition(
79
+ wav=torch.zeros(*cond.wav.shape[:-1], 1, device=cond.wav.device),
80
+ length=torch.zeros(B, dtype=cond.length.dtype, device=cond.wav.device),
81
+ sample_rate=cond.sample_rate,
82
+ path=[None] * B,
83
+ seek_time=[None] * B,
84
+ )
85
+
86
+
87
+ def nullify_all_conditions(
88
+ samples: list[ConditioningAttributes],
89
+ ) -> list[ConditioningAttributes]:
90
+ """Return a copy of ``samples`` with every wav/text condition nulled out.
91
+
92
+ Used to build the unconditional batch for classifier-free guidance.
93
+ """
94
+ samples = deepcopy(samples)
95
+ for sample in samples:
96
+ for k in list(sample.wav):
97
+ sample.wav[k] = nullify_wav(sample.wav[k])
98
+ for k in list(sample.text):
99
+ sample.text[k] = None
100
+ return samples
101
+
102
+
103
+ class MelSpectrogramConditioner(nn.Module):
104
+ """Log mel spectrogram conditioner. Projects mel bins to transformer dim."""
105
+
106
+ def __init__(
107
+ self,
108
+ output_dim: int,
109
+ device: torch.device | str,
110
+ sample_rate: int,
111
+ n_fft: int = 2048,
112
+ frame_rate: int = 100,
113
+ n_mel_bins: int = 512,
114
+ normalize_audio: bool = False,
115
+ log_scale: bool = True,
116
+ eps: float = 1e-6,
117
+ # unused arg kept for config compatibility
118
+ fine_frame_rate: int = None,
119
+ ):
120
+ self.fine_frame_rate_ratio = 1
121
+ if fine_frame_rate is not None:
122
+ assert fine_frame_rate % frame_rate == 0
123
+ self.fine_frame_rate_ratio = fine_frame_rate // frame_rate
124
+
125
+ super().__init__()
126
+ self.dim = n_mel_bins * self.fine_frame_rate_ratio
127
+ self.output_dim = output_dim
128
+ self.output_proj = nn.Linear(self.dim, output_dim)
129
+ self.device = device
130
+ self.sample_rate = sample_rate
131
+ self.frame_rate = frame_rate
132
+ self.normalize_audio = normalize_audio
133
+ self.log_scale = log_scale
134
+ self.eps = eps
135
+
136
+ if self.fine_frame_rate_ratio == 1:
137
+ assert sample_rate % frame_rate == 0
138
+ self.hop_length = sample_rate // frame_rate
139
+ else:
140
+ assert sample_rate % fine_frame_rate == 0
141
+ self.hop_length = sample_rate // fine_frame_rate
142
+
143
+ self.mel_spec_transform = _MelSpectrogram(
144
+ sample_rate=sample_rate,
145
+ n_fft=n_fft,
146
+ hop_length=self.hop_length,
147
+ n_mels=n_mel_bins,
148
+ power=1.0,
149
+ center=True,
150
+ pad_mode="reflect",
151
+ ).to(device)
152
+
153
+ def tokenize(self, x: WavCondition) -> WavCondition:
154
+ wav, length, sample_rate, path, seek_time = x
155
+ assert length is not None
156
+ return WavCondition(
157
+ wav.to(self.device), length.to(self.device), sample_rate, path, seek_time
158
+ )
159
+
160
+ def _mel_embedding(self, x: WavCondition) -> torch.Tensor:
161
+ if x.wav.shape[-1] == 1:
162
+ return torch.zeros(x.wav.shape[0], 1, self.dim, device=self.device)
163
+ if torch.cuda.is_available():
164
+ torch.cuda.synchronize()
165
+ t0 = time.perf_counter()
166
+ with torch.no_grad():
167
+ wav = x.wav
168
+ if self.normalize_audio:
169
+ wav = wav / (wav.abs().max(dim=-1, keepdim=True).values + 1e-8)
170
+ mel = self.mel_spec_transform(wav)
171
+ mel = rearrange(mel, "b 1 d t -> b t d")
172
+ if self.fine_frame_rate_ratio > 1:
173
+ mel = rearrange(
174
+ mel[:, :-1], "b (t f) d -> b t (f d)", f=self.fine_frame_rate_ratio
175
+ )
176
+ if self.log_scale:
177
+ mel = torch.log(mel + self.eps)
178
+ if torch.cuda.is_available():
179
+ torch.cuda.synchronize()
180
+ print(
181
+ f"[muscriptor] mel-spec ({wav.shape[0]} × {wav.shape[-1]} samples): "
182
+ f"{time.perf_counter() - t0:.3f}s"
183
+ )
184
+ return mel
185
+
186
+ def forward(self, x: WavCondition) -> ConditionType:
187
+ _, lengths, *_ = x
188
+ with torch.no_grad():
189
+ embeds = self._mel_embedding(x)
190
+ embeds = embeds.to(self.output_proj.weight)
191
+ embeds = self.output_proj(embeds)
192
+
193
+ if lengths is not None:
194
+ lengths = lengths / (self.sample_rate // self.frame_rate)
195
+ mask = length_to_mask(lengths, max_len=embeds.shape[1]).int()
196
+ else:
197
+ mask = torch.ones_like(embeds[..., 0])
198
+ mask_f = mask.float().unsqueeze(-1).to(embeds.device)
199
+ embeds = embeds * mask_f
200
+ return embeds, mask
201
+
202
+
203
+ class ClassConditioner(nn.Module):
204
+ """Conditioner that embeds class indices (e.g., instrument group, dataset name)."""
205
+
206
+ def __init__(
207
+ self,
208
+ num_classes: int,
209
+ output_dim: int,
210
+ device: torch.device | str = "cpu",
211
+ ):
212
+ super().__init__()
213
+ self.device = device
214
+ self.embed = nn.Embedding(num_classes + 1, output_dim).to(device)
215
+ self.pad_idx = 0
216
+
217
+ def tokenize(self, x: list[str | None]) -> torch.Tensor:
218
+ int_x = [list(map(int, s.split())) if s is not None else [-1] for s in x]
219
+ max_len = max(len(xi) for xi in int_x)
220
+ int_x = [xi + [-1] * (max_len - len(xi)) for xi in int_x]
221
+ int_x = 1 + torch.LongTensor(int_x).to(self.device)
222
+ return int_x
223
+
224
+ def forward(self, inputs: torch.Tensor) -> ConditionType:
225
+ embeds = self.embed(inputs + 1)
226
+ mask = torch.ones_like(embeds[..., 0])
227
+ return embeds, mask
228
+
229
+
230
+ def collate_wavs(
231
+ samples: list[ConditioningAttributes], wav_conditions: list[str]
232
+ ) -> dict[str, WavCondition]:
233
+ """Collate wav conditions from a list of ConditioningAttributes."""
234
+ wavs = defaultdict(list)
235
+ lengths = defaultdict(list)
236
+ sample_rates: dict[str, list] = defaultdict(list)
237
+ paths: dict[str, list] = defaultdict(list)
238
+ seek_times: dict[str, list] = defaultdict(list)
239
+ out: dict[str, WavCondition] = {}
240
+
241
+ for sample in samples:
242
+ for attribute in wav_conditions:
243
+ wav, length, sample_rate, path, seek_time = sample.wav[attribute]
244
+ assert wav.dim() == 3
245
+ B, K, T = wav.shape
246
+ assert B == 1
247
+ if K == 2:
248
+ wav = wav.mean(1, keepdim=True)
249
+ wavs[attribute].append(wav)
250
+ lengths[attribute].append(length)
251
+ sample_rates[attribute].extend(sample_rate)
252
+ paths[attribute].extend(path)
253
+ seek_times[attribute].extend(seek_time)
254
+
255
+ for attribute in wav_conditions:
256
+ # Stack along batch dim
257
+ all_wavs = wavs[attribute]
258
+ max_len = max(w.shape[-1] for w in all_wavs)
259
+ padded = torch.cat(
260
+ [F.pad(w, (0, max_len - w.shape[-1])) for w in all_wavs], dim=0
261
+ )
262
+ out[attribute] = WavCondition(
263
+ padded,
264
+ torch.cat(lengths[attribute]),
265
+ sample_rates[attribute],
266
+ paths[attribute],
267
+ seek_times[attribute],
268
+ )
269
+ return out
270
+
271
+
272
+ class ConditioningProvider(nn.Module):
273
+ """Runs all conditioners and returns a dict of condition tensors."""
274
+
275
+ def __init__(
276
+ self,
277
+ conditioners: dict[str, nn.Module],
278
+ device: torch.device | str = "cpu",
279
+ ):
280
+ super().__init__()
281
+ self.device = device
282
+ self.conditioners = nn.ModuleDict(conditioners)
283
+
284
+ @property
285
+ def text_conditions(self):
286
+ return [
287
+ k for k, v in self.conditioners.items() if isinstance(v, ClassConditioner)
288
+ ]
289
+
290
+ @property
291
+ def wav_conditions(self):
292
+ return [
293
+ k
294
+ for k, v in self.conditioners.items()
295
+ if isinstance(v, MelSpectrogramConditioner)
296
+ ]
297
+
298
+ def tokenize(self, inputs: list[ConditioningAttributes]) -> dict[str, Any]:
299
+ output = {}
300
+ # Collate text conditions
301
+ text_batch: dict[str, list[str | None]] = defaultdict(list)
302
+ for sample in inputs:
303
+ for cond in self.text_conditions:
304
+ text_batch[cond].append(sample.text.get(cond))
305
+ for attr, batch in text_batch.items():
306
+ output[attr] = self.conditioners[attr].tokenize(batch)
307
+
308
+ # Collate wav conditions
309
+ if self.wav_conditions:
310
+ wav_batch = collate_wavs(inputs, self.wav_conditions)
311
+ for attr, wav_cond in wav_batch.items():
312
+ output[attr] = self.conditioners[attr].tokenize(wav_cond)
313
+
314
+ return output
315
+
316
+ def forward(self, tokenized: dict[str, Any]) -> dict[str, ConditionType]:
317
+ output = {}
318
+ for attribute, inputs in tokenized.items():
319
+ condition, mask = self.conditioners[attribute](inputs)
320
+ output[attribute] = (condition, mask)
321
+ return output
@@ -0,0 +1,106 @@
1
+ """Standalone pure-torch equivalent of torchaudio.transforms.MelSpectrogram.
2
+
3
+ Matches torchaudio with mel_scale='htk', norm=None, win_length == n_fft.
4
+ """
5
+
6
+ import torch
7
+ from torch import nn
8
+
9
+
10
+ def _hz_to_mel_htk(freq: torch.Tensor) -> torch.Tensor:
11
+ return 2595.0 * torch.log10(1.0 + freq / 700.0)
12
+
13
+
14
+ def _mel_to_hz_htk(mel: torch.Tensor) -> torch.Tensor:
15
+ return 700.0 * (10 ** (mel / 2595.0) - 1.0)
16
+
17
+
18
+ def melscale_fbanks(
19
+ n_freqs: int,
20
+ f_min: float,
21
+ f_max: float,
22
+ n_mels: int,
23
+ sample_rate: int,
24
+ ) -> torch.Tensor:
25
+ """Triangular mel filterbank matching torchaudio.functional.melscale_fbanks
26
+ with mel_scale='htk' and norm=None. Returns a tensor of shape [n_freqs, n_mels]."""
27
+ all_freqs = torch.linspace(0, sample_rate // 2, n_freqs)
28
+ m_min = _hz_to_mel_htk(torch.tensor(float(f_min)))
29
+ m_max = _hz_to_mel_htk(torch.tensor(float(f_max)))
30
+ m_pts = torch.linspace(m_min.item(), m_max.item(), n_mels + 2)
31
+ f_pts = _mel_to_hz_htk(m_pts)
32
+
33
+ f_diff = f_pts[1:] - f_pts[:-1]
34
+ slopes = f_pts.unsqueeze(0) - all_freqs.unsqueeze(1)
35
+ down_slopes = -slopes[:, :-2] / f_diff[:-1]
36
+ up_slopes = slopes[:, 2:] / f_diff[1:]
37
+ return torch.maximum(torch.zeros(()), torch.minimum(down_slopes, up_slopes))
38
+
39
+
40
+ class _Spectrogram(nn.Module):
41
+ """Holds the STFT window so the safetensors key
42
+ `...mel_spec_transform.spectrogram.window` round-trips."""
43
+
44
+ def __init__(self, n_fft: int):
45
+ super().__init__()
46
+ self.register_buffer("window", torch.hann_window(n_fft))
47
+
48
+
49
+ class _MelScale(nn.Module):
50
+ """Holds the mel filterbank so the safetensors key
51
+ `...mel_spec_transform.mel_scale.fb` round-trips."""
52
+
53
+ def __init__(self, fb: torch.Tensor):
54
+ super().__init__()
55
+ self.register_buffer("fb", fb)
56
+
57
+
58
+ class _MelSpectrogram(nn.Module):
59
+ """Pure-torch equivalent of torchaudio.transforms.MelSpectrogram
60
+ (htk mel scale, no Slaney norm, win_length == n_fft)."""
61
+
62
+ def __init__(
63
+ self,
64
+ sample_rate: int,
65
+ n_fft: int,
66
+ hop_length: int,
67
+ n_mels: int,
68
+ power: float = 2.0,
69
+ center: bool = True,
70
+ pad_mode: str = "reflect",
71
+ ):
72
+ super().__init__()
73
+ self.n_fft = n_fft
74
+ self.hop_length = hop_length
75
+ self.power = power
76
+ self.center = center
77
+ self.pad_mode = pad_mode
78
+
79
+ self.spectrogram = _Spectrogram(n_fft)
80
+ fb = melscale_fbanks(
81
+ n_freqs=n_fft // 2 + 1,
82
+ f_min=0.0,
83
+ f_max=sample_rate / 2.0,
84
+ n_mels=n_mels,
85
+ sample_rate=sample_rate,
86
+ )
87
+ self.mel_scale = _MelScale(fb)
88
+
89
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
90
+ leading = x.shape[:-1]
91
+ x = x.reshape(-1, x.shape[-1])
92
+ spec = torch.stft(
93
+ x,
94
+ n_fft=self.n_fft,
95
+ hop_length=self.hop_length,
96
+ win_length=self.n_fft,
97
+ window=self.spectrogram.window,
98
+ center=self.center,
99
+ pad_mode=self.pad_mode,
100
+ return_complex=True,
101
+ normalized=False,
102
+ onesided=True,
103
+ )
104
+ spec = spec.abs() ** self.power
105
+ mel = torch.matmul(spec.transpose(-1, -2), self.mel_scale.fb).transpose(-1, -2)
106
+ return mel.reshape(*leading, *mel.shape[-2:])
@@ -0,0 +1,68 @@
1
+ """Stateful module API.
2
+
3
+ Each :class:`StatefulModule` exposes :meth:`init_state` returning a dict of
4
+ per-module tensors. :func:`init_states` walks an ``nn.Module`` tree, calls
5
+ ``init_state`` on every stateful submodule, and returns a ``dict[name -> state]``
6
+ that callers thread through ``forward`` via a ``model_state`` argument.
7
+
8
+ State is mutated only by :meth:`increment_step` (called explicitly via
9
+ :func:`increment_steps`) and by ``forward`` writing into preallocated buffers
10
+ at known offsets. No magic context manager, no implicit per-module storage.
11
+ """
12
+
13
+ from abc import ABC, abstractmethod
14
+ from typing import Any
15
+ from torch import nn
16
+
17
+
18
+ State = dict[str, Any]
19
+ ModelState = dict[str, State]
20
+
21
+
22
+ class StatefulModule(ABC, nn.Module):
23
+ def __init__(self, *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ self._module_absolute_name: str | None = None
26
+
27
+ @abstractmethod
28
+ def init_state(self, batch_size: int, sequence_length: int) -> State:
29
+ raise NotImplementedError
30
+
31
+ def increment_step(self, state: State, increment: int = 1) -> None:
32
+ pass
33
+
34
+ def get_state(self, model_state: ModelState | None) -> State | None:
35
+ if model_state is None or self._module_absolute_name is None:
36
+ return None
37
+ return model_state.get(self._module_absolute_name)
38
+
39
+
40
+ def init_states(model: nn.Module, batch_size: int, sequence_length: int) -> ModelState:
41
+ """Allocate state for every :class:`StatefulModule` reachable from ``model``.
42
+
43
+ Side effect: each stateful submodule has its ``_module_absolute_name`` set
44
+ so subsequent ``get_state`` calls can find its slot.
45
+ """
46
+ result: ModelState = {}
47
+ for module_name, module in model.named_modules():
48
+ if isinstance(module, StatefulModule):
49
+ module._module_absolute_name = module_name
50
+ result[module_name] = module.init_state(batch_size, sequence_length)
51
+ return result
52
+
53
+
54
+ def increment_steps(
55
+ model: nn.Module, model_state: ModelState, increment: int = 1
56
+ ) -> None:
57
+ """Bump the step counter for every stateful submodule of ``model``.
58
+
59
+ Uses each module's ``_module_absolute_name`` (set by :func:`init_states`)
60
+ to look up its slot, so this works on subtrees even when ``init_states``
61
+ was called on a different root.
62
+ """
63
+ for _, module in model.named_modules():
64
+ if (
65
+ isinstance(module, StatefulModule)
66
+ and module._module_absolute_name is not None
67
+ ):
68
+ module.increment_step(model_state[module._module_absolute_name], increment)
@@ -0,0 +1,202 @@
1
+ """Causal streaming transformer for muscriptor inference."""
2
+
3
+ from einops import rearrange
4
+ import torch
5
+ import torch.nn as nn
6
+ from torch.nn import functional as F
7
+
8
+ from muscriptor.modules.streaming import ModelState, State, StatefulModule
9
+
10
+
11
+ def create_sin_embedding(
12
+ positions: torch.Tensor,
13
+ dim: int,
14
+ max_period: float = 10000,
15
+ dtype: torch.dtype = torch.float32,
16
+ ) -> torch.Tensor:
17
+ assert dim % 2 == 0
18
+ half_dim = dim // 2
19
+ positions = positions.to(dtype)
20
+ adim = torch.arange(half_dim, device=positions.device, dtype=dtype).view(1, 1, -1)
21
+ max_period_tensor = torch.full([], max_period, device=positions.device, dtype=dtype)
22
+ phase = positions / (max_period_tensor ** (adim / (half_dim - 1)))
23
+ return torch.cat([torch.cos(phase), torch.sin(phase)], dim=-1)
24
+
25
+
26
+ class StreamingMultiheadAttention(StatefulModule):
27
+ """Causal multi-head self-attention with a preallocated KV cache."""
28
+
29
+ def __init__(
30
+ self,
31
+ embed_dim: int,
32
+ num_heads: int,
33
+ device=None,
34
+ dtype=None,
35
+ ):
36
+ super().__init__()
37
+ factory_kwargs = {"device": device, "dtype": dtype}
38
+ self.embed_dim = embed_dim
39
+ self.num_heads = num_heads
40
+ self.dim_per_head = embed_dim // num_heads
41
+
42
+ in_proj = nn.Linear(embed_dim, 3 * embed_dim, bias=False, **factory_kwargs)
43
+ self.in_proj_weight = in_proj.weight
44
+ self.in_proj_bias = in_proj.bias
45
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False, **factory_kwargs)
46
+
47
+ def init_state(self, batch_size: int, sequence_length: int) -> State:
48
+ weight = self.in_proj_weight
49
+ return {
50
+ "cache": torch.full(
51
+ (2, batch_size, sequence_length, self.num_heads, self.dim_per_head),
52
+ float("nan"),
53
+ device=weight.device,
54
+ dtype=weight.dtype,
55
+ ),
56
+ "offset": torch.zeros(1, dtype=torch.long, device=weight.device),
57
+ }
58
+
59
+ def increment_step(self, state: State, increment: int = 1) -> None:
60
+ state["offset"] = state["offset"] + increment
61
+
62
+ def _complete_kv(self, k, v, state: State | None):
63
+ if state is None:
64
+ return k, v
65
+ cache = state["cache"]
66
+ end = int(state["offset"].item())
67
+ T = k.shape[1]
68
+ cache[0, :, end : end + T] = k
69
+ cache[1, :, end : end + T] = v
70
+ return cache[0, :, : end + T], cache[1, :, : end + T]
71
+
72
+ def forward(
73
+ self,
74
+ query: torch.Tensor,
75
+ model_state: ModelState | None = None,
76
+ ):
77
+ state = self.get_state(model_state)
78
+ projected = nn.functional.linear(query, self.in_proj_weight)
79
+ packed = rearrange(projected, "b t (p h d) -> b t p h d", p=3, h=self.num_heads)
80
+ q, k, v = packed.unbind(dim=2)
81
+
82
+ k, v = self._complete_kv(k, v, state)
83
+ dtype = q.dtype
84
+
85
+ q_t = q.transpose(1, 2)
86
+ k_t = k.transpose(1, 2)
87
+ v_t = v.transpose(1, 2)
88
+
89
+ # Explicit bottom-right causal mask so streaming decode steps
90
+ # (T_q=1, T_k=cache_len) attend to all past tokens. PyTorch's
91
+ # is_causal=True uses top-left alignment and would mask out all
92
+ # cached tokens except position 0 when T_q < T_k.
93
+ T_q, T_k = q_t.shape[2], k_t.shape[2]
94
+ mask = torch.ones(T_q, T_k, dtype=torch.bool, device=query.device).tril(
95
+ T_k - T_q
96
+ )
97
+ attn_bias = torch.zeros(T_q, T_k, dtype=q.dtype, device=query.device)
98
+ attn_bias.masked_fill_(~mask, float("-inf"))
99
+ x = F.scaled_dot_product_attention(
100
+ q_t, k_t, v_t, attn_mask=attn_bias, dropout_p=0.0
101
+ )
102
+ x = x.transpose(1, 2).to(dtype)
103
+
104
+ x = rearrange(x, "b t h d -> b t (h d)")
105
+ x = self.out_proj(x)
106
+ return x
107
+
108
+
109
+ class StreamingTransformerLayer(nn.Module):
110
+ """Pre-norm transformer block: self-attention + GELU FFN."""
111
+
112
+ def __init__(
113
+ self,
114
+ d_model: int,
115
+ num_heads: int,
116
+ dim_feedforward: int = 2048,
117
+ device=None,
118
+ dtype=None,
119
+ ):
120
+ super().__init__()
121
+ factory_kwargs = {"device": device, "dtype": dtype}
122
+ self.self_attn = StreamingMultiheadAttention(
123
+ embed_dim=d_model, num_heads=num_heads, **factory_kwargs
124
+ )
125
+ self.norm1 = nn.LayerNorm(d_model, eps=1e-5, **factory_kwargs)
126
+ self.norm2 = nn.LayerNorm(d_model, eps=1e-5, **factory_kwargs)
127
+ self.linear1 = nn.Linear(d_model, dim_feedforward, bias=False, **factory_kwargs)
128
+ self.linear2 = nn.Linear(dim_feedforward, d_model, bias=False, **factory_kwargs)
129
+
130
+ def forward(
131
+ self,
132
+ x: torch.Tensor,
133
+ model_state: ModelState | None = None,
134
+ ):
135
+ x = x + self.self_attn(self.norm1(x), model_state=model_state)
136
+ x = x + self.linear2(F.gelu(self.linear1(self.norm2(x))))
137
+ return x
138
+
139
+
140
+ class StreamingTransformer(StatefulModule):
141
+ """Stack of causal streaming transformer layers with sinusoidal positions."""
142
+
143
+ def __init__(
144
+ self,
145
+ d_model: int,
146
+ num_heads: int,
147
+ num_layers: int,
148
+ dim_feedforward: int = 2048,
149
+ max_period: float = 10_000,
150
+ device=None,
151
+ dtype=None,
152
+ ):
153
+ super().__init__()
154
+ assert d_model % num_heads == 0
155
+ self.max_period = max_period
156
+ self.layers = nn.ModuleList(
157
+ [
158
+ StreamingTransformerLayer(
159
+ d_model=d_model,
160
+ num_heads=num_heads,
161
+ dim_feedforward=dim_feedforward,
162
+ device=device,
163
+ dtype=dtype,
164
+ )
165
+ for _ in range(num_layers)
166
+ ]
167
+ )
168
+
169
+ def init_state(self, batch_size: int, sequence_length: int) -> State:
170
+ device = self.layers[0].norm2.weight.device
171
+ return {
172
+ "offsets": torch.zeros(batch_size, dtype=torch.long, device=device),
173
+ }
174
+
175
+ def increment_step(self, state: State, increment: int = 1) -> None:
176
+ state["offsets"] = state["offsets"] + increment
177
+
178
+ def forward(
179
+ self,
180
+ x: torch.Tensor,
181
+ prepend_length: int = 0,
182
+ model_state: ModelState | None = None,
183
+ ):
184
+ del prepend_length # unused; positions come from state['offsets']
185
+ B, T, C = x.shape
186
+ state = self.get_state(model_state)
187
+ offsets = (
188
+ state["offsets"]
189
+ if state is not None
190
+ else torch.zeros(B, dtype=torch.long, device=x.device)
191
+ )
192
+
193
+ positions = torch.arange(T, device=x.device).view(1, -1, 1)
194
+ positions = positions + offsets.view(-1, 1, 1)
195
+ pos_emb = create_sin_embedding(
196
+ positions, C, max_period=self.max_period, dtype=x.dtype
197
+ )
198
+ x = x + pos_emb * (positions >= 0).float()
199
+
200
+ for layer in self.layers:
201
+ x = layer(x, model_state=model_state)
202
+ return x