Skip to content

Commit d83e22a

Browse files
authored
Merge demo changes (#123)
* add reset to demo * Fix README and run * add short delay to control_signal detection * Fix incorrect commit * rm script * add support for pedal sampling * update model implementation (training) to fp32 rotary embeddings * fix arg * fix grad_acc_steps in lr decay * add dynamic playback support to demo * fix mido race condition * rm * fix mido deadlock in MIDI IO * add token masks * fixes * add bf16 support * add flag for back-and-forth mode * add config override to demo * minor qol changes * update README * update README
1 parent 90db087 commit d83e22a

12 files changed

Lines changed: 1241 additions & 535 deletions

File tree

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,27 +79,40 @@ Our embedding model was trained to capture composition-level and performance-lev
7979

8080
## Real-time demo
8181

82-
In `demo/` we provide CUDA (Linux/PyTorch) and MLX (Apple Silicon) implementations of the real-time interactive piano-continuation demo showcased in our release blog post. For the demo we used an acoustic Yamaha Disklavier piano with simultaneous MIDI input and output ports connected via a standard MIDI interface.
82+
In `demo/` we provide an MLX (Apple Silicon) implementation of the real-time interactive piano-continuation demo showcased in our release blog post. In order to use the demo, you must download the demo-specific model checkpoint which enhances the model to additionally control the sustain pedal ([direct-download](https://huggingface.co/loubb/aria-medium-base/resolve/main/model-demo.safetensors?download=true)).
8383

84-
**NOTE**: Responsiveness of the real-time demo is dependent on your system configuration, e.g., GPU FLOPS and memory bandwidth.
84+
For our demonstration, we used an acoustic Yamaha Disklavier piano with simultaneous MIDI input and output ports connected via a standard MIDI interface. We disabled the built-in Disklavier playback mode, instead manually calibrating key-velocity latency to enhance responsiveness. You may recreate this in your own environment with our acoustic calibration settings, using the following script:
8585

86-
A MIDI input device is not strictly required to play around with the demo: By using the `--midi_path` and `--midi_through` arguments you can mock real-time input by playing from a MIDI file. All that is required are MIDI drivers (e.g., CoreMIDI, ALSA) and a virtual software instrument (e.g., Fluidsynth, Pianoteq) to render the output.
86+
**NOTE**: It is vital that you use the `latency=off`/`realtime` Disklavier playback setting when using the provided configuration for `--hardware`.
8787

88-
Example usage (MLX):
88+
```bash
89+
python ./demo/demo_mlx.py \
90+
--checkpoint <checkpoint-path> \
91+
--midi_in <midi-in-port> \
92+
--midi_out <midi-out-port> \
93+
--hardware ./demo/hardware/c4dm-disklavier.json \
94+
--midi_control_signal 67 \
95+
--midi_reset_control_signal 66 \
96+
--temp 0.9 \
97+
--min_p 0.03
98+
```
99+
100+
A MIDI input device is not strictly required to play around with the demo: By using the `--midi_path` and `--midi_through` arguments you can mock real-time input by playing from a MIDI file. All that is required are MIDI drivers (e.g., CoreMIDI) and a virtual software instrument (e.g., Fluidsynth, Pianoteq) to render the output. In this mode, you can initiate the model takeover by pressing the enter key.
89101

90102
```bash
91-
MIDI_PATH="example-prompts/pokey_jazz.mid"
103+
MIDI_PATH="./example-prompts/smooth_jazz.mid"
92104

93-
python demo/demo_mlx.py \
105+
python ./demo/demo_mlx.py \
94106
--checkpoint <checkpoint-path> \
95107
--midi_path ${MIDI_PATH} \
96-
--midi_through <port-to-stream-midi-file-through> \
97-
--midi_out <port-to-stream-generation-over> \
98-
--save_path <path-to-save-result> \
99-
--temp 0.98 \
100-
--min_p 0.035
108+
--midi_through <midi-playback-port> \
109+
--midi_out <midi-playback-port> \
110+
--temp 0.9 \
111+
--min_p 0.03
101112
```
102113

114+
**NOTE**: Responsiveness of the real-time demo is dependent on your system configuration, specifically GPU memory bandwidth.
115+
103116
## Evaluation
104117

105118
We provide the specific files/splits we used for Aria-MIDI derived linear-probe and classification evaluations. These can be downloaded from HuggingFace ([direct-download](https://huggingface.co/loubb/aria-medium-base/resolve/main/eval-splits.tar.gz?download=true)). Class labels are provided in `metadata.json` with the schema:

aria/inference/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,21 @@ def get_inference_prompt(
4848
for msg in midi_dict.note_msgs
4949
if midi_dict.tick_to_ms(msg["data"]["start"]) <= prompt_len_ms
5050
]
51+
midi_dict.pedal_msgs = [
52+
msg
53+
for msg in midi_dict.pedal_msgs
54+
if midi_dict.tick_to_ms(msg["tick"]) <= prompt_len_ms
55+
]
56+
if midi_dict.pedal_msgs and midi_dict.pedal_msgs[-1]["data"] == 1:
57+
midi_dict.pedal_msgs.pop()
5158

5259
if len(midi_dict.note_msgs) == 0:
5360
return [("prefix", "instrument", "piano"), tokenizer.bos_tok]
5461

55-
seq = tokenizer.tokenize(midi_dict=midi_dict, add_dim_tok=False)
56-
seq.remove(tokenizer.eos_tok)
62+
seq = tokenizer.tokenize(
63+
midi_dict=midi_dict,
64+
add_dim_tok=False,
65+
add_eos_tok=False,
66+
)
5767

5868
return seq

aria/inference/model_mlx.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,30 +84,41 @@ def __call__(
8484
self,
8585
x: mx.array,
8686
input_pos: mx.array,
87+
max_kv_pos: int | None,
8788
offset: int,
8889
mask: mx.array,
8990
):
9091
assert self.kv_cache is not None, "Cache not initialized"
91-
9292
x += self._att_block(
9393
x=self.norm1(x),
9494
input_pos=input_pos,
95+
max_kv_pos=max_kv_pos,
9596
offset=offset,
9697
mask=mask,
9798
)
9899
x = x + self._ff_block(self.norm2(x))
99100

100101
return x
101102

102-
def get_kv(self, k: mx.array, v: mx.array, input_pos: mx.array):
103+
def get_kv(
104+
self,
105+
k: mx.array,
106+
v: mx.array,
107+
input_pos: mx.array,
108+
max_kv_pos: int | None,
109+
):
103110
k, v = self.kv_cache.update(k_val=k, v_val=v, input_pos=input_pos)
104111

105-
return k, v
112+
if max_kv_pos is not None:
113+
return k[:, :, : max_kv_pos + 1, :], v[:, :, : max_kv_pos + 1, :]
114+
else:
115+
return k, v
106116

107117
def _att_block(
108118
self,
109119
x: mx.array,
110120
input_pos: mx.array,
121+
max_kv_pos: int | None,
111122
offset: int,
112123
mask: mx.array,
113124
):
@@ -124,7 +135,8 @@ def _att_block(
124135
k = apply_rotary_emb_mlx(k, offset=offset)
125136
q, k, v = map(lambda x: x.transpose(0, 2, 1, 3), (q, k, v))
126137

127-
k, v = self.get_kv(k, v, input_pos=input_pos)
138+
k, v = self.get_kv(k, v, input_pos=input_pos, max_kv_pos=max_kv_pos)
139+
128140
wv = mx.fast.scaled_dot_product_attention(
129141
q=q,
130142
k=k,
@@ -159,6 +171,7 @@ def __init__(self, model_config: ModelConfig):
159171
TransformerBlock(model_config) for _ in range(model_config.n_layers)
160172
]
161173
self.out_layer_norm = nn.LayerNorm(model_config.d_model)
174+
self.kv_ctx = None
162175

163176
def fill_condition_kv(self, emb: mx.array):
164177
assert self.causal_mask is not None, "Caches must be initialized first"
@@ -177,20 +190,30 @@ def __call__(
177190
self,
178191
idxs: mx.array,
179192
input_pos: mx.array,
193+
max_kv_pos: int,
180194
offset: int,
181195
pad_idxs: mx.array | None = None,
196+
_debug_track_kv: bool = False,
182197
):
183198
assert self.causal_mask is not None, "Caches must be initialized first"
184199

185-
mask = self.causal_mask[None, None, input_pos]
200+
if self.kv_ctx is None:
201+
self.kv_ctx = mx.full(
202+
self.model_config.max_seq_len, 3
203+
) # unk_tok id
204+
205+
if _debug_track_kv is True:
206+
self.kv_ctx[input_pos] = idxs
207+
self.kv_ctx[input_pos[-1].item() + 1 :] = 3
186208

209+
mask = self.causal_mask[None, None, input_pos, : max_kv_pos + 1]
187210
if pad_idxs is not None:
188211
pad_mask = mx.expand_dims(mx.expand_dims(pad_idxs, axis=1), axis=1)
189212
mask = mask & ~pad_mask
190213

191214
x = self.tok_embeddings(idxs)
192215
for layer in self.encode_layers:
193-
x = layer(x, input_pos, offset, mask)
216+
x = layer(x, input_pos, max_kv_pos, offset, mask)
194217

195218
x = self.out_layer_norm(x)
196219

@@ -217,11 +240,13 @@ def __call__(
217240
idxs: mx.array,
218241
input_pos: mx.array,
219242
offset: int,
243+
max_kv_pos: int | None = None,
220244
pad_idxs: mx.array | None = None,
221245
):
222246
hidden_states = self.model(
223247
idxs=idxs,
224248
input_pos=input_pos,
249+
max_kv_pos=max_kv_pos,
225250
offset=offset,
226251
pad_idxs=pad_idxs,
227252
)
@@ -235,6 +260,25 @@ def fill_condition_kv(self, cond_emb: mx.array):
235260
adapted_emb = self.embedding_adapter(cond_emb)
236261
self.model.fill_condition_kv(emb=adapted_emb)
237262

263+
def reset_kv_ctx(self):
264+
self.model.kv_ctx = None
265+
266+
def get_kv_ctx(self):
267+
# Used for debugging kv-cache validation
268+
_kv_ctx = self.model.kv_ctx
269+
270+
match self.model.kv_ctx:
271+
case None:
272+
return None
273+
case mx.array():
274+
_kv_ctx = self.model.kv_ctx.tolist()
275+
if 3 in _kv_ctx:
276+
return _kv_ctx[: _kv_ctx.index(3)]
277+
else:
278+
return _kv_ctx
279+
case _:
280+
raise ValueError
281+
238282
def setup_cache(
239283
self,
240284
batch_size,

aria/inference/sample_cuda.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@
1616
DTYPE = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
1717

1818

19-
def get_cfg_prompt(prompts: list):
20-
cfg_prompts = []
21-
for prompt in prompts:
22-
cfg_prompts.append(prompt)
23-
cfg_prompts.append(prompt)
24-
25-
return cfg_prompts
26-
27-
2819
@torch.inference_mode()
2920
def decode_one(
3021
model: TransformerLM,

aria/model.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ def forward(
178178
seq_len=self.model_config.max_seq_len,
179179
n_elem=self.model_config.d_model // self.model_config.n_heads,
180180
base=500000,
181-
dtype=hidden_states.dtype,
182181
).to(src.device)
183182
freqs_cis = self.freqs_cis[: src.shape[1]]
184183

@@ -379,7 +378,6 @@ def precompute_freqs_cis(
379378
seq_len: int,
380379
n_elem: int,
381380
base: int = 500000,
382-
dtype: torch.dtype = torch.bfloat16,
383381
):
384382
freqs = 1.0 / (
385383
base ** (torch.arange(0, n_elem, 2)[: (n_elem // 2)].float() / n_elem)
@@ -389,22 +387,23 @@ def precompute_freqs_cis(
389387
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
390388
cache = torch.stack([freqs_cis.real, freqs_cis.imag], dim=-1)
391389

392-
return cache.to(dtype=dtype)
390+
return cache
393391

394392

395393
@torch.jit.script
396394
def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
397395
"""
398396
In-place RoPE. Credits to Katherine Crowson:
399397
x shape (b_sz, s_len, n_head, d_head).
400-
cos, sin shape (s_len, d_head // 2).
398+
freqs_cis shape (s_len, d_head // 2, 2) and is float32.
401399
"""
402-
403-
d = x.shape[-1] // 2
400+
x_float = x.float()
401+
freqs_cis = freqs_cis.detach()
402+
d = x_float.shape[-1] // 2
404403
cos = freqs_cis[..., 0][None, :, None]
405404
sin = freqs_cis[..., 1][None, :, None]
406-
x1, x2 = x[..., :d], x[..., d : d * 2]
405+
x1, x2 = x_float[..., :d], x_float[..., d : d * 2]
407406
tmp = x1.clone()
408407
x1.mul_(cos).addcmul_(x2, sin, value=-1)
409408
x2.mul_(cos).addcmul_(tmp, sin, value=1)
410-
return x
409+
return x.copy_(x_float)

aria/run.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def generate(args):
263263
args.prompt_midi_path,
264264
prompt_duration_s=prompt_duration_s,
265265
)
266+
print(prompt)
266267
max_new_tokens = min(8096 - len(prompt), max_new_tokens)
267268

268269
if backend == "torch_cuda":
@@ -317,13 +318,13 @@ def generate(args):
317318

318319

319320
def _get_embedding(
320-
embedding_model_checkpoints_path: str,
321+
embedding_model_checkpoint_path: str,
321322
embedding_midi_path: str,
322323
):
323324
from aria.embedding import get_global_embedding_from_midi
324325

325326
model = _load_embedding_model(
326-
checkpoint_path=embedding_model_checkpoints_path
327+
checkpoint_path=embedding_model_checkpoint_path
327328
).cpu()
328329
global_embedding = get_global_embedding_from_midi(
329330
model=model,
@@ -353,7 +354,7 @@ def conditioned_generate(args):
353354
prompt_duration_s=prompt_duration_s,
354355
)
355356
embedding = _get_embedding(
356-
embedding_model_checkpoints_path=args.embedding_model_checkpoint_path,
357+
embedding_model_checkpoint_path=args.embedding_model_checkpoint_path,
357358
embedding_midi_path=args.embedding_midi_path,
358359
)
359360
max_new_tokens = min(8096 - len(prompt), max_new_tokens)

aria/training/train.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ def resume_train(
586586
optimizer, scheduler = get_optim(
587587
model,
588588
num_epochs=epochs,
589-
steps_per_epoch=len(train_dataloader),
589+
steps_per_epoch=len(train_dataloader) // grad_acc_steps,
590590
)
591591

592592
(
@@ -731,7 +731,7 @@ def train(
731731
optimizer, scheduler = get_optim(
732732
model,
733733
num_epochs=epochs,
734-
steps_per_epoch=len(train_dataloader),
734+
steps_per_epoch=len(train_dataloader) // grad_acc_steps,
735735
)
736736

737737
(

config/models/medium.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"ff_mult": 4,
66
"drop_p": 0.0,
77
"max_seq_len": 8192,
8+
"vocab_size": 17727,
89
"grad_checkpoint": true
910
}

0 commit comments

Comments
 (0)