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,514 @@
1
+ """Language model for MIDI token generation.
2
+
3
+ Adapted from audiocraft/models/lm.py.
4
+ """
5
+
6
+ import logging
7
+ import time
8
+ from collections.abc import Iterator
9
+
10
+ import torch
11
+ from torch import nn
12
+
13
+ from muscriptor.modules.conditioners import (
14
+ ConditioningProvider,
15
+ ConditioningAttributes,
16
+ ConditionType,
17
+ nullify_all_conditions,
18
+ )
19
+ from muscriptor.modules.streaming import (
20
+ ModelState,
21
+ increment_steps,
22
+ init_states,
23
+ )
24
+ from muscriptor.modules.transformer import StreamingTransformer
25
+ import muscriptor.utils.sampling as utils
26
+
27
+
28
+ logger = logging.getLogger(__name__)
29
+ ConditionTensors = dict[str, ConditionType]
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # ScaledEmbedding (used for token embeddings, keeps weight compatible with ckpt)
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ class ScaledEmbedding(nn.Embedding):
38
+ """Embedding that maps zero_idx (a negative index) to a zero vector."""
39
+
40
+ def __init__(self, *args, zero_idx: int = -1, **kwargs):
41
+ super().__init__(*args, **kwargs)
42
+ assert zero_idx < 0
43
+ self.zero_idx = zero_idx
44
+
45
+ def forward(self, input, *args, **kwargs):
46
+ is_zero = input == self.zero_idx
47
+ input = input.clamp(min=0)
48
+ y = super().forward(input, *args, **kwargs)
49
+ return torch.where(is_zero[..., None], torch.zeros_like(y), y)
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # TorchAutocast
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ class TorchAutocast:
58
+ """Minimal autocast context manager (matches the audiocraft interface)."""
59
+
60
+ def __init__(
61
+ self,
62
+ enabled: bool = False,
63
+ device_type: str = "cuda",
64
+ dtype: torch.dtype | None = None,
65
+ ):
66
+ self.enabled = enabled
67
+ self.device_type = device_type
68
+ self.dtype = dtype
69
+ self._ctx = None
70
+
71
+ def __enter__(self):
72
+ if self.enabled:
73
+ self._ctx = torch.autocast(device_type=self.device_type, dtype=self.dtype)
74
+ self._ctx.__enter__()
75
+ return self
76
+
77
+ def __exit__(self, *args):
78
+ if self.enabled and self._ctx is not None:
79
+ self._ctx.__exit__(*args)
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # LMModel
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ class LMModel(nn.Module):
88
+ """Causal transformer LM for MIDI token generation.
89
+
90
+ Single-stream
91
+ Supports classifier-free guidance at inference time.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ condition_provider: ConditioningProvider,
97
+ card: int = 1024,
98
+ dim: int = 128,
99
+ num_heads: int = 8,
100
+ hidden_scale: int = 4,
101
+ cfg_coef: float = 1.0,
102
+ autocast: TorchAutocast | None = None,
103
+ device=None,
104
+ dtype=None,
105
+ **kwargs,
106
+ ):
107
+ super().__init__()
108
+ self.condition_provider = condition_provider
109
+ self.card = card
110
+ self.dim = dim
111
+ self.cfg_coef = cfg_coef
112
+ self.autocast = (
113
+ autocast if autocast is not None else TorchAutocast(enabled=True)
114
+ )
115
+
116
+ self.emb = ScaledEmbedding(
117
+ self.card + 1,
118
+ dim,
119
+ device=device,
120
+ dtype=dtype,
121
+ zero_idx=self.zero_token_id,
122
+ )
123
+
124
+ self.transformer = StreamingTransformer(
125
+ d_model=dim,
126
+ num_heads=num_heads,
127
+ dim_feedforward=int(hidden_scale * dim),
128
+ device=device,
129
+ dtype=dtype,
130
+ **kwargs,
131
+ )
132
+ self.out_norm = nn.LayerNorm(dim, eps=1e-5)
133
+ self.linear = nn.Linear(dim, card, bias=False)
134
+
135
+ # ------------------------------------------------------------------
136
+ # Token ID properties
137
+ # ------------------------------------------------------------------
138
+
139
+ @property
140
+ def initial_token_id(self) -> int:
141
+ return self.card
142
+
143
+ @property
144
+ def zero_token_id(self) -> int:
145
+ return -1
146
+
147
+ @property
148
+ def ungenerated_token_id(self) -> int:
149
+ return -2
150
+
151
+ # ------------------------------------------------------------------
152
+ # Forward
153
+ # ------------------------------------------------------------------
154
+
155
+ def forward(
156
+ self,
157
+ sequence: torch.Tensor, # [B, S]
158
+ condition_tensors: ConditionTensors,
159
+ first_step: bool = False,
160
+ model_state: ModelState | None = None,
161
+ ) -> torch.Tensor: # [B, S, card]
162
+ B, S = sequence.shape
163
+
164
+ input_ = self.emb(sequence) # [B, S, D]
165
+
166
+ prepend_length = 0
167
+ if first_step:
168
+ for cond, _ in condition_tensors.values():
169
+ input_ = torch.cat([cond, input_], dim=1)
170
+ prepend_length = input_.shape[1] - S
171
+
172
+ transformer_out = self.transformer(
173
+ input_,
174
+ prepend_length=prepend_length,
175
+ model_state=model_state,
176
+ )
177
+ if self.out_norm:
178
+ transformer_out = self.out_norm(transformer_out)
179
+
180
+ # Remove prepended conditioning tokens
181
+ if prepend_length > 0:
182
+ transformer_out = transformer_out[:, -S:]
183
+
184
+ logits = self.linear(transformer_out)
185
+ return logits # [B, S, card]
186
+
187
+ # ------------------------------------------------------------------
188
+ # Sampling helpers
189
+ # ------------------------------------------------------------------
190
+
191
+ def _compute_logits(
192
+ self,
193
+ sequence: torch.Tensor,
194
+ cfg_conditions: ConditionTensors,
195
+ model_state: ModelState,
196
+ first_step: bool,
197
+ cfg_coef: float | None = None,
198
+ ) -> torch.Tensor: # [B, card]
199
+ """Run the forward pass and return masked logits at the last timestep."""
200
+ B = sequence.shape[0]
201
+ cfg_coef = self.cfg_coef if cfg_coef is None else cfg_coef
202
+
203
+ if cfg_coef == 1.0:
204
+ logits = self(
205
+ sequence,
206
+ cfg_conditions,
207
+ first_step=first_step,
208
+ model_state=model_state,
209
+ )
210
+ else:
211
+ doubled = torch.cat([sequence, sequence], dim=0)
212
+ all_logits = self(
213
+ doubled,
214
+ cfg_conditions,
215
+ first_step=first_step,
216
+ model_state=model_state,
217
+ )
218
+ cond_logits, uncond_logits = all_logits.split(B, dim=0)
219
+ logits = uncond_logits + (cond_logits - uncond_logits) * cfg_coef
220
+
221
+ logits = logits[:, -1, :].float() # [B, card] — last timestep
222
+ logits[:, 1393:] = -torch.inf # mask reserved / OOV tokens
223
+ return logits
224
+
225
+ def _sample_next_token(
226
+ self,
227
+ sequence: torch.Tensor,
228
+ cfg_conditions: ConditionTensors,
229
+ model_state: ModelState,
230
+ first_step: bool,
231
+ use_sampling: bool = False,
232
+ temp: float = 1.0,
233
+ top_k: int = 0,
234
+ top_p: float = 0.0,
235
+ cfg_coef: float | None = None,
236
+ ) -> torch.Tensor: # [B]
237
+ logits = self._compute_logits(
238
+ sequence, cfg_conditions, model_state, first_step, cfg_coef
239
+ )
240
+ if use_sampling and temp > 0.0:
241
+ probs = torch.softmax(logits / temp, dim=-1)
242
+ next_tokens = utils.sample_from_probs(probs, top_p=top_p, top_k=top_k)[:, 0]
243
+ else:
244
+ next_tokens = torch.argmax(logits, dim=-1) # [B]
245
+ return next_tokens # [B]
246
+
247
+ # ------------------------------------------------------------------
248
+ # Generation
249
+ # ------------------------------------------------------------------
250
+
251
+ @torch.no_grad()
252
+ def generate(
253
+ self,
254
+ prompt: torch.Tensor | None = None,
255
+ conditions: list[ConditioningAttributes] = [],
256
+ num_samples: int | None = None,
257
+ max_gen_len: int = 256,
258
+ use_sampling: bool = True,
259
+ temp: float = 1.0,
260
+ top_k: int = 0,
261
+ top_p: float = 0.0,
262
+ cfg_coef: float | None = None,
263
+ early_stop_on_token: int | None = None,
264
+ beam_size: int = 1,
265
+ beam_length_score_alpha: float = 0.75,
266
+ ) -> Iterator[torch.Tensor]:
267
+ """Autoregressively generate tokens, yielding one timestep at a time.
268
+
269
+ Each yield is a ``[num_samples]`` tensor. For beam_size == 1 (default),
270
+ tokens are yielded as they are generated. For beam_size > 1, beam search
271
+ is run non-streamingly and all tokens are yielded at the end.
272
+ """
273
+ assert not self.training
274
+ if beam_size > 1:
275
+ assert early_stop_on_token is not None, "beam search requires early_stop_on_token"
276
+ device = self.emb.weight.device
277
+
278
+ if num_samples is None:
279
+ num_samples = (
280
+ len(conditions)
281
+ if conditions
282
+ else (prompt.shape[0] if prompt is not None else 1)
283
+ )
284
+
285
+ cfg_coef = self.cfg_coef if cfg_coef is None else cfg_coef
286
+
287
+ # Build condition tensors (with null conditions appended for CFG)
288
+ if conditions:
289
+ if cfg_coef == 1.0:
290
+ prepared = self.condition_provider.tokenize(conditions)
291
+ if torch.cuda.is_available():
292
+ torch.cuda.synchronize()
293
+ _t = time.perf_counter()
294
+ cfg_conditions: ConditionTensors = self.condition_provider(prepared)
295
+ if torch.cuda.is_available():
296
+ torch.cuda.synchronize()
297
+ print(
298
+ f"[muscriptor] encode conditions (total): {time.perf_counter() - _t:.3f}s"
299
+ )
300
+ else:
301
+ null_conditions = nullify_all_conditions(conditions)
302
+ all_conditions = conditions + null_conditions
303
+ prepared = self.condition_provider.tokenize(all_conditions)
304
+ print(
305
+ "[muscriptor] instrument_group tokens:",
306
+ prepared.get("instrument_group"),
307
+ )
308
+ print(
309
+ "[muscriptor] dataset_name tokens: ",
310
+ prepared.get("dataset_name"),
311
+ )
312
+ if torch.cuda.is_available():
313
+ torch.cuda.synchronize()
314
+ _t = time.perf_counter()
315
+ cfg_conditions = self.condition_provider(prepared)
316
+ if torch.cuda.is_available():
317
+ torch.cuda.synchronize()
318
+ print(
319
+ f"[muscriptor] encode conditions (total): {time.perf_counter() - _t:.3f}s"
320
+ )
321
+ else:
322
+ cfg_conditions = {}
323
+
324
+ eff_batch = num_samples * beam_size
325
+
326
+ # Expand conditions so each beam gets its own copy (interleaved for CFG).
327
+ if beam_size > 1 and cfg_conditions:
328
+ cfg_conditions = {
329
+ k: (
330
+ torch.repeat_interleave(cond, beam_size, dim=0),
331
+ torch.repeat_interleave(mask, beam_size, dim=0),
332
+ )
333
+ for k, (cond, mask) in cfg_conditions.items()
334
+ }
335
+
336
+ # Initialise generation buffer (eff_batch rows = num_samples × beam_size)
337
+ ungenerated = self.ungenerated_token_id
338
+ gen_sequence = torch.full(
339
+ (eff_batch, max_gen_len + 1),
340
+ ungenerated,
341
+ device=device,
342
+ dtype=torch.long,
343
+ )
344
+ gen_sequence[:, 0] = self.initial_token_id
345
+
346
+ start_offset = 0
347
+ if prompt is not None:
348
+ PT = prompt.shape[-1]
349
+ if beam_size > 1:
350
+ prompt = torch.repeat_interleave(prompt, beam_size, dim=0)
351
+ gen_sequence[:, 1 : 1 + PT] = prompt
352
+ ungenerated_steps = (gen_sequence == ungenerated).nonzero()[:, 1]
353
+ start_offset = max(0, int(ungenerated_steps.amin()) - 1)
354
+
355
+ prepend_length = sum(cond.shape[1] for cond, _ in cfg_conditions.values())
356
+ cache_batch_size = eff_batch * (1 if cfg_coef == 1.0 else 2)
357
+ cache_seq_len = prepend_length + max_gen_len
358
+ model_state = init_states(
359
+ self, batch_size=cache_batch_size, sequence_length=cache_seq_len
360
+ )
361
+
362
+ # Accumulated log-prob scores, one per beam row.
363
+ beam_scores = torch.zeros(eff_batch, device=device, dtype=torch.float)
364
+
365
+ # For greedy/sampling emit prompt steps now; beam search emits at the end.
366
+ if beam_size == 1:
367
+ for t in range(start_offset):
368
+ yield gen_sequence[:, t + 1]
369
+
370
+ last_offset = start_offset - 1
371
+ with self.autocast:
372
+ for offset in range(start_offset, max_gen_len):
373
+ last_offset = offset
374
+ first_iter = offset == start_offset
375
+ input_ = (
376
+ gen_sequence[:, : offset + 1]
377
+ if first_iter
378
+ else gen_sequence[:, offset : offset + 1]
379
+ )
380
+
381
+ if beam_size == 1:
382
+ # ── Standard greedy / sampling path ──────────────────
383
+ if early_stop_on_token is not None:
384
+ done = (gen_sequence == early_stop_on_token).any(dim=1).all()
385
+ if done:
386
+ break
387
+
388
+ next_token = self._sample_next_token(
389
+ input_,
390
+ cfg_conditions,
391
+ model_state,
392
+ first_step=first_iter,
393
+ use_sampling=use_sampling,
394
+ temp=temp,
395
+ top_k=top_k,
396
+ top_p=top_p,
397
+ cfg_coef=cfg_coef,
398
+ ) # [B]
399
+
400
+ input_T = input_.shape[-1]
401
+ increment_steps(
402
+ self.transformer,
403
+ model_state,
404
+ increment=input_T + (prepend_length if first_iter else 0),
405
+ )
406
+
407
+ this_gen_step = gen_sequence[:, offset + 1]
408
+ next_token = torch.where(
409
+ this_gen_step == ungenerated, next_token, this_gen_step
410
+ )
411
+ gen_sequence[:, offset + 1] = next_token
412
+
413
+ yield gen_sequence[:, offset + 1] # [num_samples]
414
+
415
+ else:
416
+ # ── Beam search step ──────────────────────────────────
417
+ logits = self._compute_logits(
418
+ input_, cfg_conditions, model_state,
419
+ first_step=first_iter, cfg_coef=cfg_coef,
420
+ ) # [eff_batch, card]
421
+ input_T = input_.shape[-1]
422
+ increment_steps(
423
+ self.transformer,
424
+ model_state,
425
+ increment=input_T + (prepend_length if first_iter else 0),
426
+ )
427
+
428
+ log_probs = torch.log_softmax(logits.float(), dim=-1)
429
+
430
+ # Top beam_size candidate tokens per current beam
431
+ topk_scores, topk_tokens = torch.topk(log_probs, k=beam_size, dim=-1)
432
+
433
+ # Track which beams have already emitted EOS
434
+ eos_mask = gen_sequence == early_stop_on_token
435
+ beam_has_ended = eos_mask.any(dim=-1)
436
+ eos_pos = eos_mask.int().argmax(dim=-1).clamp(min=1)
437
+ beam_lengths = torch.where(
438
+ beam_has_ended, eos_pos,
439
+ torch.full_like(eos_pos, offset + 1),
440
+ )
441
+
442
+ # Finished beams: don't expand further
443
+ topk_scores = torch.where(
444
+ beam_has_ended.unsqueeze(-1),
445
+ torch.zeros_like(topk_scores),
446
+ topk_scores,
447
+ )
448
+
449
+ # Length-normalized candidate scores: [eff_batch, beam_size]
450
+ lp = 1.0 / (beam_lengths.float() ** beam_length_score_alpha)
451
+ cand = (beam_scores.unsqueeze(-1) + topk_scores) * lp.unsqueeze(-1)
452
+
453
+ # Reshape to [num_samples, beam_size²] for cross-beam selection
454
+ cand_2d = cand.reshape(num_samples, beam_size * beam_size)
455
+
456
+ if offset == start_offset:
457
+ # All beams identical at start — take first beam_size tokens
458
+ new_scores = cand_2d[:, :beam_size]
459
+ best_idx = (
460
+ torch.arange(beam_size, device=device)
461
+ .unsqueeze(0)
462
+ .expand(num_samples, -1)
463
+ )
464
+ else:
465
+ new_scores, best_idx = torch.topk(cand_2d, k=beam_size, dim=-1)
466
+
467
+ # Decode flat index → (prev_beam_within_sample, token_rank)
468
+ prev_local = (best_idx // beam_size).reshape(-1)
469
+ tok_rank = (best_idx % beam_size).reshape(-1)
470
+
471
+ # Map to global row indices in [eff_batch, …] tensors
472
+ sample_base = (
473
+ torch.arange(num_samples, device=device)
474
+ .repeat_interleave(beam_size) * beam_size
475
+ )
476
+ prev_global = sample_base + prev_local
477
+
478
+ # Token for each new beam
479
+ next_token = topk_tokens[prev_global, tok_rank]
480
+
481
+ # Update beam scores (store un-normalized for the next step)
482
+ beam_scores = new_scores.reshape(-1) / lp[prev_global]
483
+
484
+ # Reorder generation sequences to match winning beams
485
+ gen_sequence = gen_sequence[prev_global]
486
+
487
+ # Reorder KV caches — shape is [2, batch, T, heads, head_dim]
488
+ for state in model_state.values():
489
+ if "cache" in state:
490
+ cache = state["cache"]
491
+ if cache.shape[1] == 2 * eff_batch: # CFG-doubled cache
492
+ reorder = torch.cat([prev_global, prev_global + eff_batch])
493
+ else:
494
+ reorder = prev_global
495
+ state["cache"] = cache[:, reorder, :, :, :]
496
+
497
+ # Write next token (respecting pre-filled prompt positions)
498
+ this_step = gen_sequence[:, offset + 1]
499
+ next_token = torch.where(this_step == ungenerated, next_token, this_step)
500
+ gen_sequence[:, offset + 1] = next_token
501
+
502
+ # Early stop when every beam in every sample has emitted EOS
503
+ if (gen_sequence == early_stop_on_token).any(dim=-1).all():
504
+ break
505
+
506
+ # Beam search: select best beam per sample and yield all tokens at once
507
+ if beam_size > 1:
508
+ best_beam = beam_scores.reshape(num_samples, beam_size).argmax(dim=-1)
509
+ best_global = (
510
+ torch.arange(num_samples, device=device) * beam_size + best_beam
511
+ )
512
+ best_sequence = gen_sequence[best_global] # [num_samples, T]
513
+ for t in range(last_offset + 1):
514
+ yield best_sequence[:, t + 1]
File without changes