scragnog commited on
Commit
45ed331
·
verified ·
1 Parent(s): d84671b

hotstep-v1: soft-distilled + calibrated pooled-v4

Browse files
README.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: minimax-music3-terms
4
+ license_link: https://huggingface.co/MiniMaxAI/MiniMax-Music3
5
+ base_model: Mothersuperior/open-rvq-encoder-minimax-music3-169m-pooled-v4
6
+ datasets:
7
+ - Mothersuperior/minimax-music3-rvq-distill-corpus-8k
8
+ tags:
9
+ - minimax-music3
10
+ - rvq
11
+ - audio-encoder
12
+ - reverse-distillation
13
+ - knowledge-distillation
14
+ ---
15
+
16
+ # open-rvq-encoder-minimax-music3-169m — soft-distilled + calibrated (hotstep-v1)
17
+
18
+ [Mothersuperior/open-rvq-encoder-minimax-music3-169m-pooled-v4](https://huggingface.co/Mothersuperior/open-rvq-encoder-minimax-music3-169m-pooled-v4)
19
+ further-trained with a **pure soft-distillation objective** (soft cross-entropy against the teacher
20
+ top-50 distributions shipped in the 8k corpus) plus **learnable per-head logit temperatures**, then
21
+ selected on a **real-audio gate** that no other release in this lineage measures. Architecture,
22
+ config, and file format are identical to the base — this is a drop-in replacement.
23
+
24
+ Three things in this release are useful even if you never load the weights:
25
+
26
+ ## 1. Corpus consumption fixes (apply these before training on the 8k corpus)
27
+
28
+ **uint16 wrap in `probs.npz` semantic ids.** The semantic head's `idx` values were stored as raw
29
+ LM token ids (`code + 151675`) in a uint16, which silently wraps them mod 65536. They arrive as
30
+ `code + 20603`. Subtract 20603 and mask anything outside `[0, 16384)` (the EOS token wraps to −5).
31
+ Verified: after unwrapping, the sampled ground-truth code lands inside the teacher top-50 for
32
+ ~100% of frames; consumed naively, the ids are garbage and soft-distillation losses are meaningless.
33
+
34
+ **Stitched-timeline latent alignment.** The corpus audio is stitched from 200-frame DiT windows at
35
+ a 100-frame / **345-latent** hop (documented in the corpus card), while the naive uniform mapping
36
+ (`latent = frame * 441 // 128` ≈ 344.53/100 frames) drifts ~1 latent per 800 frames. Measured
37
+ effect: semantic top-1 against ground-truth codes decays from 0.41 at track start to 0.06 by frame
38
+ 2400 under the uniform rule. The correct frame→latent mapping for this corpus is:
39
+
40
+ ```python
41
+ def stitched_lat(i): # frame index -> DAV latent index
42
+ k = 0 if i < 125 else (i - 25) // 100 # chunk ownership (from local frame 25)
43
+ return 345 * k + ((i - 100 * k) * 441) // 128 + 1
44
+ ```
45
+
46
+ The `+1` is a constant global offset, verified empirically as best at every probed position on
47
+ every probed record (top-1 flat at 0.4–0.6 across track positions once applied). The corpus's own
48
+ DAV latents are exact — recomputing them from the shipped FLAC through the official `dav.pth`
49
+ reproduces them at cosine 1.0 — the drift lives in the audio-vs-codes timeline of chunked
50
+ rendering. **Real audio has no such drift**: the uniform mapping remains correct at inference.
51
+
52
+ ## 2. Calibrated logits (folded)
53
+
54
+ The released v4/pooled-v4 readout logits are muP-sharp and uncalibrated: harmless for
55
+ argmax/top-K use (what the reference adapter and the ComfyUI/diffusers nodes do), but
56
+ cross-entropy and softmax probabilities computed from them are meaningless (semantic soft-CE ~59
57
+ nats at temperature 1; per-frame logit std ~45). This release trained **learnable per-head
58
+ temperatures** jointly with the model (initialised from a line search: ~16 semantic, ~2 acoustic)
59
+ and **folded them into the head weights** at export. Argmax and top-K ordering are unchanged;
60
+ `softmax(logits)` and CE are now meaningful out of the box. The final folded values are in
61
+ `calibration.json`.
62
+
63
+ ## 3. The weights
64
+
65
+ Training: pure soft-CE vs teacher top-50 (all 8 heads; depth decoder teacher-forced), 20,000 steps
66
+ at effective batch 64 (16 × 4 grad-accum), lr 1e-4 cosine with 200-step warmup, bf16, single
67
+ RTX 5090, on the drift-corrected + unwrapped 8k corpus (records < 8 s filtered; 7,726 train / 394
68
+ holdout by salted-hash split). Trainer included in [`training/`](training/).
69
+
70
+ ### In-domain holdout (8k corpus, drift-corrected loader, calibrated CE)
71
+
72
+ | holdout metric | pooled-v4 (start) | this release (step 18500) | change |
73
+ |---|---|---|---|
74
+ | semantic soft-CE (vs teacher) | 3.299 | 3.039 | −7.9% |
75
+ | acoustic soft-CE | 4.618 | 4.413 | −4.4% |
76
+ | semantic hard-CE | 2.336 | 2.154 | −7.8% |
77
+ | acoustic hard-CE | 3.925 | 3.763 | −4.1% |
78
+ | semantic top-1 | 41.9% | 42.2% | +0.3 pt |
79
+ | acoustic top-1 | 16.8% | 17.6% | +0.8 pt |
80
+
81
+ In-domain argmax barely moves — the interesting result is below.
82
+
83
+ ### Real-audio gate (13 commercial studio recordings, frozen official LM/depth decoder base-CE)
84
+
85
+ The metric nobody else in the lineage reports: export codes from real recordings, measure the
86
+ frozen official model's cross-entropy on them. Lower = the encoder's codes look more like codes
87
+ the official model would emit. All prior rows reproduced on the same harness:
88
+
89
+ | encoder | params | sem CE | sem t1 | ac CE | ac t1 |
90
+ |---|---|---|---|---|---|
91
+ | Serveurperso community 41M | 41M | 8.447 | 5.8% | 6.763 | 2.3% |
92
+ | Mothersuperior pooled-v3 | 41M | 7.261 | 8.5% | 6.347 | 4.5% |
93
+ | SimpleTuner v4 | 169M | 7.158 | 8.9% | 5.638 | 5.8% |
94
+ | Mothersuperior pooled-v4 | 169M | 6.838 | 10.9% | 5.373 | 7.5% |
95
+ | **this release** | **169M** | **5.866** | **16.6%** | **4.725** | **10.9%** |
96
+
97
+ The largest single-step improvement in the lineage on this metric — and it happens at the
98
+ argmax level (temperature folding cannot cause it), despite near-flat in-domain argmax
99
+ accuracy. Soft distillation appears to act as regularisation against synthetic-audio overfit,
100
+ transferring to real recordings. Caveat: semantic unique-code usage on the real-audio set drops
101
+ ~14% vs the base (6,490 vs 7,515 over 64k frames; acoustic unchanged) — mild concentration,
102
+ far from collapse, but stated for transparency.
103
+
104
+ Ear-verified on frame-matched replay renders of five real commercial recordings across four
105
+ genres (indie rock, pop, alt-metal, electronic): an audible improvement over pooled-v4 on all
106
+ of them, though subtler to the ear than the pooled-v4 → v4 step was — consistent with
107
+ diminishing returns from a synthetic-only corpus at this size. Best in the lineage by both
108
+ metric and ear; the remaining real-audio fidelity gap likely needs real-audio objectives
109
+ (consistency training, LM-prior regularisation, reconstruction through the frozen generator)
110
+ rather than more synthetic epochs.
111
+
112
+ ## Usage
113
+
114
+ Identical to the base model. Load with SimpleTuner's
115
+ [`minimax_music3_reference_adapter.py`](https://huggingface.co/SimpleTuner/open-rvq-encoder-minimax-music3/blob/main/minimax_music3_reference_adapter.py):
116
+
117
+ ```python
118
+ from minimax_music3_reference_adapter import MiniMaxMusic3ReferenceAdapter
119
+ adapter = MiniMaxMusic3ReferenceAdapter.from_files(
120
+ "rvq_encoder.safetensors", "rvq_encoder_config.json", "dav.pth")
121
+ codes = adapter.predict_codes(waveform, sample_rate) # [frames, 8]
122
+ ```
123
+
124
+ `dav.pth` is the official DAV encoder from
125
+ [MiniMaxAI/MiniMax-Music3](https://huggingface.co/MiniMaxAI/MiniMax-Music3).
126
+ Strict `load_state_dict` — the checkpoint keys match the base exactly.
127
+
128
+ ## Further training
129
+
130
+ [`training/rvq_distill_train.py`](training/rvq_distill_train.py) is the self-contained trainer
131
+ used for this release: 8k-corpus zip layout, both consumption fixes above built in, warm start
132
+ from any encoder in this lineage, pure-soft or mixed soft/hard loss, learnable temperatures with
133
+ fold-at-save, holdout eval + best-checkpoint selection. Depends only on torch/numpy/safetensors
134
+ plus the reference adapter module for the model classes.
135
+
136
+ ## Credits
137
+
138
+ This encoder exists because of a chain of community work, each stage building on the last:
139
+
140
+ - **[MiniMax](https://huggingface.co/MiniMaxAI/MiniMax-Music3)** — MiniMax Music 3 and the DAV
141
+ encoder. All weights and training data derive from it; use is subject to its terms.
142
+ - **[Serveurperso](https://github.com/ServeurpersoCom)** — the original community proof: a 41M
143
+ single-GPU encoder demonstrating that exact token agreement was not required, and the first
144
+ independent derivation of the stitched-timeline alignment contract.
145
+ - **[bghira / SimpleTuner](https://huggingface.co/SimpleTuner/open-rvq-encoder-minimax-music3)** —
146
+ the encoder architecture (v1–v4, causal depth decoder), the reference adapter, the trainer, and
147
+ the [reverse-distillation trace corpus](https://huggingface.co/datasets/bghira/minimax-music3-rvq-reverse-distillation).
148
+ - **[Mothersuperior](https://huggingface.co/Mothersuperior)** — the
149
+ [8k distillation corpus](https://huggingface.co/datasets/Mothersuperior/minimax-music3-rvq-distill-corpus-8k)
150
+ with teacher top-50 distributions (which make this release's objective possible), and the
151
+ pooled-v3/pooled-v4 fine-tunes this release warm-starts from.
152
+ - **[HOT-Step CPP](https://github.com/scragnog/HOT-Step-CPP)** — this release: the soft-distillation
153
+ training run, the corpus consumption fixes, logit calibration, and the real-audio gate.
154
+
155
+ Use is subject to the MiniMax Music 3 model terms and the source datasets' terms.
calibration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"folded_taus": [8.8834, 1.3853, 1.4031, 1.3873, 1.4341, 1.4484, 1.4719, 1.4799]}
evaluation/train_log.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
mup_base_shapes.bsh ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This is a base shape file encoded in yaml
2
+ # - `null` indicates a dimension is "finite", i.e. a non-"width" dimension
3
+ # - a number indicates the base dimension of an "infinite" dimension, i.e. some notion of "width"
4
+ blocks.0.conv1.bias:
5
+ - 128
6
+ blocks.0.conv1.weight:
7
+ - 128
8
+ - 128
9
+ - null
10
+ blocks.0.conv2.bias:
11
+ - 128
12
+ blocks.0.conv2.weight:
13
+ - 128
14
+ - 128
15
+ - null
16
+ blocks.0.norm.bias:
17
+ - 128
18
+ blocks.0.norm.weight:
19
+ - 128
20
+ blocks.1.conv1.bias:
21
+ - 128
22
+ blocks.1.conv1.weight:
23
+ - 128
24
+ - 128
25
+ - null
26
+ blocks.1.conv2.bias:
27
+ - 128
28
+ blocks.1.conv2.weight:
29
+ - 128
30
+ - 128
31
+ - null
32
+ blocks.1.norm.bias:
33
+ - 128
34
+ blocks.1.norm.weight:
35
+ - 128
36
+ blocks.2.conv1.bias:
37
+ - 128
38
+ blocks.2.conv1.weight:
39
+ - 128
40
+ - 128
41
+ - null
42
+ blocks.2.conv2.bias:
43
+ - 128
44
+ blocks.2.conv2.weight:
45
+ - 128
46
+ - 128
47
+ - null
48
+ blocks.2.norm.bias:
49
+ - 128
50
+ blocks.2.norm.weight:
51
+ - 128
52
+ conv_in.bias:
53
+ - 128
54
+ conv_in.weight:
55
+ - 128
56
+ - null
57
+ - null
58
+ depth_decoder.context_projection.weight:
59
+ - null
60
+ - 128
61
+ depth_decoder.heads.0.bias:
62
+ - null
63
+ depth_decoder.heads.0.weight:
64
+ - null
65
+ - null
66
+ depth_decoder.heads.1.bias:
67
+ - null
68
+ depth_decoder.heads.1.weight:
69
+ - null
70
+ - null
71
+ depth_decoder.heads.2.bias:
72
+ - null
73
+ depth_decoder.heads.2.weight:
74
+ - null
75
+ - null
76
+ depth_decoder.heads.3.bias:
77
+ - null
78
+ depth_decoder.heads.3.weight:
79
+ - null
80
+ - null
81
+ depth_decoder.heads.4.bias:
82
+ - null
83
+ depth_decoder.heads.4.weight:
84
+ - null
85
+ - null
86
+ depth_decoder.heads.5.bias:
87
+ - null
88
+ depth_decoder.heads.5.weight:
89
+ - null
90
+ - null
91
+ depth_decoder.heads.6.bias:
92
+ - null
93
+ depth_decoder.heads.6.weight:
94
+ - null
95
+ - null
96
+ depth_decoder.layers.0.k_proj.bias:
97
+ - null
98
+ depth_decoder.layers.0.k_proj.weight:
99
+ - null
100
+ - null
101
+ depth_decoder.layers.0.linear1.bias:
102
+ - null
103
+ depth_decoder.layers.0.linear1.weight:
104
+ - null
105
+ - null
106
+ depth_decoder.layers.0.linear2.bias:
107
+ - null
108
+ depth_decoder.layers.0.linear2.weight:
109
+ - null
110
+ - null
111
+ depth_decoder.layers.0.norm1.bias:
112
+ - null
113
+ depth_decoder.layers.0.norm1.weight:
114
+ - null
115
+ depth_decoder.layers.0.norm2.bias:
116
+ - null
117
+ depth_decoder.layers.0.norm2.weight:
118
+ - null
119
+ depth_decoder.layers.0.out_proj.bias:
120
+ - null
121
+ depth_decoder.layers.0.out_proj.weight:
122
+ - null
123
+ - null
124
+ depth_decoder.layers.0.q_proj.bias:
125
+ - null
126
+ depth_decoder.layers.0.q_proj.weight:
127
+ - null
128
+ - null
129
+ depth_decoder.layers.0.v_proj.bias:
130
+ - null
131
+ depth_decoder.layers.0.v_proj.weight:
132
+ - null
133
+ - null
134
+ depth_decoder.layers.1.k_proj.bias:
135
+ - null
136
+ depth_decoder.layers.1.k_proj.weight:
137
+ - null
138
+ - null
139
+ depth_decoder.layers.1.linear1.bias:
140
+ - null
141
+ depth_decoder.layers.1.linear1.weight:
142
+ - null
143
+ - null
144
+ depth_decoder.layers.1.linear2.bias:
145
+ - null
146
+ depth_decoder.layers.1.linear2.weight:
147
+ - null
148
+ - null
149
+ depth_decoder.layers.1.norm1.bias:
150
+ - null
151
+ depth_decoder.layers.1.norm1.weight:
152
+ - null
153
+ depth_decoder.layers.1.norm2.bias:
154
+ - null
155
+ depth_decoder.layers.1.norm2.weight:
156
+ - null
157
+ depth_decoder.layers.1.out_proj.bias:
158
+ - null
159
+ depth_decoder.layers.1.out_proj.weight:
160
+ - null
161
+ - null
162
+ depth_decoder.layers.1.q_proj.bias:
163
+ - null
164
+ depth_decoder.layers.1.q_proj.weight:
165
+ - null
166
+ - null
167
+ depth_decoder.layers.1.v_proj.bias:
168
+ - null
169
+ depth_decoder.layers.1.v_proj.weight:
170
+ - null
171
+ - null
172
+ depth_decoder.norm.bias:
173
+ - null
174
+ depth_decoder.norm.weight:
175
+ - null
176
+ depth_decoder.position:
177
+ - null
178
+ - null
179
+ - null
180
+ depth_decoder.prior_embeddings.0.weight:
181
+ - null
182
+ - null
183
+ depth_decoder.prior_embeddings.1.weight:
184
+ - null
185
+ - null
186
+ depth_decoder.prior_embeddings.2.weight:
187
+ - null
188
+ - null
189
+ depth_decoder.prior_embeddings.3.weight:
190
+ - null
191
+ - null
192
+ depth_decoder.prior_embeddings.4.weight:
193
+ - null
194
+ - null
195
+ depth_decoder.prior_embeddings.5.weight:
196
+ - null
197
+ - null
198
+ depth_decoder.prior_embeddings.6.weight:
199
+ - null
200
+ - null
201
+ heads.0.bias:
202
+ - null
203
+ heads.0.weight:
204
+ - null
205
+ - 128
206
+ norm_out.bias:
207
+ - 128
208
+ norm_out.weight:
209
+ - 128
210
+ position:
211
+ - null
212
+ - null
213
+ - 128
214
+ transformer.0.k_proj.bias:
215
+ - 128
216
+ transformer.0.k_proj.weight:
217
+ - 128
218
+ - 128
219
+ transformer.0.linear1.bias:
220
+ - 512
221
+ transformer.0.linear1.weight:
222
+ - 512
223
+ - 128
224
+ transformer.0.linear2.bias:
225
+ - 128
226
+ transformer.0.linear2.weight:
227
+ - 128
228
+ - 512
229
+ transformer.0.norm1.bias:
230
+ - 128
231
+ transformer.0.norm1.weight:
232
+ - 128
233
+ transformer.0.norm2.bias:
234
+ - 128
235
+ transformer.0.norm2.weight:
236
+ - 128
237
+ transformer.0.out_proj.bias:
238
+ - 128
239
+ transformer.0.out_proj.weight:
240
+ - 128
241
+ - 128
242
+ transformer.0.q_proj.bias:
243
+ - 128
244
+ transformer.0.q_proj.weight:
245
+ - 128
246
+ - 128
247
+ transformer.0.v_proj.bias:
248
+ - 128
249
+ transformer.0.v_proj.weight:
250
+ - 128
251
+ - 128
252
+ transformer.1.k_proj.bias:
253
+ - 128
254
+ transformer.1.k_proj.weight:
255
+ - 128
256
+ - 128
257
+ transformer.1.linear1.bias:
258
+ - 512
259
+ transformer.1.linear1.weight:
260
+ - 512
261
+ - 128
262
+ transformer.1.linear2.bias:
263
+ - 128
264
+ transformer.1.linear2.weight:
265
+ - 128
266
+ - 512
267
+ transformer.1.norm1.bias:
268
+ - 128
269
+ transformer.1.norm1.weight:
270
+ - 128
271
+ transformer.1.norm2.bias:
272
+ - 128
273
+ transformer.1.norm2.weight:
274
+ - 128
275
+ transformer.1.out_proj.bias:
276
+ - 128
277
+ transformer.1.out_proj.weight:
278
+ - 128
279
+ - 128
280
+ transformer.1.q_proj.bias:
281
+ - 128
282
+ transformer.1.q_proj.weight:
283
+ - 128
284
+ - 128
285
+ transformer.1.v_proj.bias:
286
+ - 128
287
+ transformer.1.v_proj.weight:
288
+ - 128
289
+ - 128
290
+ transformer.2.k_proj.bias:
291
+ - 128
292
+ transformer.2.k_proj.weight:
293
+ - 128
294
+ - 128
295
+ transformer.2.linear1.bias:
296
+ - 512
297
+ transformer.2.linear1.weight:
298
+ - 512
299
+ - 128
300
+ transformer.2.linear2.bias:
301
+ - 128
302
+ transformer.2.linear2.weight:
303
+ - 128
304
+ - 512
305
+ transformer.2.norm1.bias:
306
+ - 128
307
+ transformer.2.norm1.weight:
308
+ - 128
309
+ transformer.2.norm2.bias:
310
+ - 128
311
+ transformer.2.norm2.weight:
312
+ - 128
313
+ transformer.2.out_proj.bias:
314
+ - 128
315
+ transformer.2.out_proj.weight:
316
+ - 128
317
+ - 128
318
+ transformer.2.q_proj.bias:
319
+ - 128
320
+ transformer.2.q_proj.weight:
321
+ - 128
322
+ - 128
323
+ transformer.2.v_proj.bias:
324
+ - 128
325
+ transformer.2.v_proj.weight:
326
+ - 128
327
+ - 128
328
+ transformer.3.k_proj.bias:
329
+ - 128
330
+ transformer.3.k_proj.weight:
331
+ - 128
332
+ - 128
333
+ transformer.3.linear1.bias:
334
+ - 512
335
+ transformer.3.linear1.weight:
336
+ - 512
337
+ - 128
338
+ transformer.3.linear2.bias:
339
+ - 128
340
+ transformer.3.linear2.weight:
341
+ - 128
342
+ - 512
343
+ transformer.3.norm1.bias:
344
+ - 128
345
+ transformer.3.norm1.weight:
346
+ - 128
347
+ transformer.3.norm2.bias:
348
+ - 128
349
+ transformer.3.norm2.weight:
350
+ - 128
351
+ transformer.3.out_proj.bias:
352
+ - 128
353
+ transformer.3.out_proj.weight:
354
+ - 128
355
+ - 128
356
+ transformer.3.q_proj.bias:
357
+ - 128
358
+ transformer.3.q_proj.weight:
359
+ - 128
360
+ - 128
361
+ transformer.3.v_proj.bias:
362
+ - 128
363
+ transformer.3.v_proj.weight:
364
+ - 128
365
+ - 128
366
+ transformer.4.k_proj.bias:
367
+ - 128
368
+ transformer.4.k_proj.weight:
369
+ - 128
370
+ - 128
371
+ transformer.4.linear1.bias:
372
+ - 512
373
+ transformer.4.linear1.weight:
374
+ - 512
375
+ - 128
376
+ transformer.4.linear2.bias:
377
+ - 128
378
+ transformer.4.linear2.weight:
379
+ - 128
380
+ - 512
381
+ transformer.4.norm1.bias:
382
+ - 128
383
+ transformer.4.norm1.weight:
384
+ - 128
385
+ transformer.4.norm2.bias:
386
+ - 128
387
+ transformer.4.norm2.weight:
388
+ - 128
389
+ transformer.4.out_proj.bias:
390
+ - 128
391
+ transformer.4.out_proj.weight:
392
+ - 128
393
+ - 128
394
+ transformer.4.q_proj.bias:
395
+ - 128
396
+ transformer.4.q_proj.weight:
397
+ - 128
398
+ - 128
399
+ transformer.4.v_proj.bias:
400
+ - 128
401
+ transformer.4.v_proj.weight:
402
+ - 128
403
+ - 128
404
+ transformer.5.k_proj.bias:
405
+ - 128
406
+ transformer.5.k_proj.weight:
407
+ - 128
408
+ - 128
409
+ transformer.5.linear1.bias:
410
+ - 512
411
+ transformer.5.linear1.weight:
412
+ - 512
413
+ - 128
414
+ transformer.5.linear2.bias:
415
+ - 128
416
+ transformer.5.linear2.weight:
417
+ - 128
418
+ - 512
419
+ transformer.5.norm1.bias:
420
+ - 128
421
+ transformer.5.norm1.weight:
422
+ - 128
423
+ transformer.5.norm2.bias:
424
+ - 128
425
+ transformer.5.norm2.weight:
426
+ - 128
427
+ transformer.5.out_proj.bias:
428
+ - 128
429
+ transformer.5.out_proj.weight:
430
+ - 128
431
+ - 128
432
+ transformer.5.q_proj.bias:
433
+ - 128
434
+ transformer.5.q_proj.weight:
435
+ - 128
436
+ - 128
437
+ transformer.5.v_proj.bias:
438
+ - 128
439
+ transformer.5.v_proj.weight:
440
+ - 128
441
+ - 128
442
+ transformer.6.k_proj.bias:
443
+ - 128
444
+ transformer.6.k_proj.weight:
445
+ - 128
446
+ - 128
447
+ transformer.6.linear1.bias:
448
+ - 512
449
+ transformer.6.linear1.weight:
450
+ - 512
451
+ - 128
452
+ transformer.6.linear2.bias:
453
+ - 128
454
+ transformer.6.linear2.weight:
455
+ - 128
456
+ - 512
457
+ transformer.6.norm1.bias:
458
+ - 128
459
+ transformer.6.norm1.weight:
460
+ - 128
461
+ transformer.6.norm2.bias:
462
+ - 128
463
+ transformer.6.norm2.weight:
464
+ - 128
465
+ transformer.6.out_proj.bias:
466
+ - 128
467
+ transformer.6.out_proj.weight:
468
+ - 128
469
+ - 128
470
+ transformer.6.q_proj.bias:
471
+ - 128
472
+ transformer.6.q_proj.weight:
473
+ - 128
474
+ - 128
475
+ transformer.6.v_proj.bias:
476
+ - 128
477
+ transformer.6.v_proj.weight:
478
+ - 128
479
+ - 128
480
+ transformer.7.k_proj.bias:
481
+ - 128
482
+ transformer.7.k_proj.weight:
483
+ - 128
484
+ - 128
485
+ transformer.7.linear1.bias:
486
+ - 512
487
+ transformer.7.linear1.weight:
488
+ - 512
489
+ - 128
490
+ transformer.7.linear2.bias:
491
+ - 128
492
+ transformer.7.linear2.weight:
493
+ - 128
494
+ - 512
495
+ transformer.7.norm1.bias:
496
+ - 128
497
+ transformer.7.norm1.weight:
498
+ - 128
499
+ transformer.7.norm2.bias:
500
+ - 128
501
+ transformer.7.norm2.weight:
502
+ - 128
503
+ transformer.7.out_proj.bias:
504
+ - 128
505
+ transformer.7.out_proj.weight:
506
+ - 128
507
+ - 128
508
+ transformer.7.q_proj.bias:
509
+ - 128
510
+ transformer.7.q_proj.weight:
511
+ - 128
512
+ - 128
513
+ transformer.7.v_proj.bias:
514
+ - 128
515
+ transformer.7.v_proj.weight:
516
+ - 128
517
+ - 128
rvq_encoder.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8e36bd36d58f467823f3e1e8c1fc4f9d3311e5527dd49768e8e7cfed68c8e31
3
+ size 676055160
rvq_encoder_config.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "codebook_vocab_sizes": [
3
+ 16384,
4
+ 1024,
5
+ 1024,
6
+ 1024,
7
+ 1024,
8
+ 1024,
9
+ 1024,
10
+ 1024
11
+ ],
12
+ "conv_dilations": [
13
+ 1,
14
+ 3,
15
+ 9
16
+ ],
17
+ "d_model": 1088,
18
+ "depth_decoder": true,
19
+ "depth_decoder_dim": 512,
20
+ "depth_decoder_dropout": 0.1,
21
+ "depth_decoder_ff_mult": 4,
22
+ "depth_decoder_heads": 8,
23
+ "depth_decoder_layers": 2,
24
+ "dropout": 0.1,
25
+ "ff_mult": 4,
26
+ "latent_channels": 128,
27
+ "max_position_embeddings": 128,
28
+ "mup": true,
29
+ "mup_attention_multiplier": 8.0,
30
+ "mup_output_mult": 1.0,
31
+ "mup_readout_zero_init": true,
32
+ "num_heads": 17,
33
+ "num_layers": 8
34
+ }
training/rvq_distill_train.py ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Further-train a SimpleTuner-class RVQ encoder on the published distillation corpora.
2
+
3
+ Warm-starts from a released checkpoint (default: Mothersuperior pooled-v4, 169M)
4
+ and trains with SOFT DISTILLATION against the teacher top-50 distributions the
5
+ corpus ships — the axis the published runs (hard-label CE, 12 epochs) never used.
6
+ Checkpoints regularly; final selection happens OFFLINE on our real-audio gate
7
+ (export_codes_v4.py --encoder-file <ckpt> + 1-step lm_sft_train2 [Base] CE),
8
+ which nobody else measures.
9
+
10
+ Corpus support:
11
+ * Mothersuperior/minimax-music3-rvq-distill-corpus-8k (PRIMARY — ships
12
+ precomputed fp16 DAV latents frame-major [F,128] @ 86.1328 Hz, plus
13
+ top-50 PROBS [T,8,50]). Records: <id>.{flac,codes.npy,probs.npz,vae.npy,json}
14
+ inside data/shard-NNN.zip; manifest.jsonl has align_ok/probs_ok flags.
15
+ Warm-up convention: codes/probs row 0 is un-emitted; frame i <-> row i+1.
16
+ * bghira/minimax-music3-rvq-reverse-distillation: NOT yet wired — ships
17
+ top-50 LOGITS but NO DAV latents, so it needs a ChunkedDAV precache pass
18
+ first (see docs/plans/2026-08-18-encoder-training-plan.md). Respect its
19
+ deterministic dataset_split when added.
20
+
21
+ Key geometry: every 128-frame window spans EXACTLY 441 DAV latents
22
+ (bounds[i] = floor(i*441/128); the +128 difference is always 441), so batches
23
+ need no padding — which matters because GroupNorm(1, C) in the conv stack
24
+ normalises over length and padding would shift its statistics.
25
+
26
+ The depth decoder is TEACHER-FORCED here (ground-truth c0..c6 as priors, causal
27
+ mask, head i read from position i+1) — the published forward() feeds its own
28
+ argmax chain and is inference-only.
29
+
30
+ Usage (single GPU, 5090):
31
+ python rvq_distill_train.py ^
32
+ --corpus M:\HOT-Step-CPP\_corpora\mm3-rvq-distill-8k ^
33
+ --encoder-dir M:\HOT-Step-CPP\_experiments\open-rvq-pooled-v4 ^
34
+ --out M:\HOT-Step-CPP\_experiments\rvq-train\pv4-softdistill-r1 ^
35
+ --steps 20000 --batch 16 --grad-accum 4 --lr 1e-4 --vram-frac 0.85
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import argparse
41
+ import hashlib
42
+ import io
43
+ import json
44
+ import math
45
+ import os
46
+ import shutil
47
+ import sys
48
+ import time
49
+ import zipfile
50
+ from pathlib import Path
51
+
52
+ import numpy as np
53
+ import torch
54
+ import torch.nn.functional as F
55
+ from torch.utils.data import DataLoader, Dataset
56
+
57
+ ADAPTER_DIR_DEFAULT = r"M:\HOT-Step-CPP\_experiments\open-rvq-v4"
58
+ WINDOW = 128
59
+ LATENTS_PER_WINDOW = 441 # exact for every start index (441/128 ratio)
60
+ TOPK = 50
61
+ SEM_TOKEN_WRAP = 151_675 % 65_536 # = 20603; see Corpus8k.__getitem__
62
+
63
+
64
+ def load_adapter_module(adapter_dir: str):
65
+ sys.path.insert(0, adapter_dir)
66
+ import minimax_music3_reference_adapter as ref
67
+
68
+ return ref
69
+
70
+
71
+ # ---------------------------------------------------------------- corpus ----
72
+
73
+ def holdout_of(record_id: str, frac: float, salt: str = "hotstep-holdout-v1") -> bool:
74
+ digest = hashlib.sha1(f"{salt}:{record_id}".encode()).digest()
75
+ return (int.from_bytes(digest[:8], "big") / 2**64) < frac
76
+
77
+
78
+ def stitched_lat(i: int) -> int:
79
+ """Frame index -> latent index in the corpus's RENDERED audio.
80
+
81
+ The 8k corpus audio is stitched from 200-frame DiT windows at a 100-frame /
82
+ 345-LATENT hop (README: 'constant stitched timeline'; non-first-chunk
83
+ ownership from local frame 25). 100 frames nominally map to 344.53 latents,
84
+ so the uniform i*441//128 mapping drifts ~1 latent per 800 and top-1 decays
85
+ to ~0 by frame 2400 — measured, then eliminated by this rule. The +1 is a
86
+ constant global offset, verified best at every position on every record
87
+ probed (t1 0.4-0.6 flat vs position). Real audio has NO such drift — the
88
+ uniform rule stays correct at inference/export time.
89
+ """
90
+ k = 0 if i < 125 else (i - 25) // 100
91
+ return 345 * k + ((i - 100 * k) * 441) // 128 + 1
92
+
93
+
94
+ def pool_matrix(bounds: list[int]) -> torch.Tensor:
95
+ """Local copy of the adapter's build_pool_matrix so DataLoader workers can
96
+ unpickle this Dataset without the runtime-sys.path adapter import."""
97
+ origin = bounds[0]
98
+ local = [b - origin for b in bounds]
99
+ pool = torch.zeros((len(local) - 1, local[-1]), dtype=torch.float32)
100
+ for i, (a, b) in enumerate(zip(local[:-1], local[1:])):
101
+ pool[i, a:b] = 1.0 / (b - a)
102
+ return pool
103
+
104
+
105
+ class Corpus8k(Dataset):
106
+ """Random 128-frame windows from the Mothersuperior 8k distill corpus."""
107
+
108
+ def __init__(self, root: str, records: list[dict], seed: int, fixed_windows: bool = False):
109
+ self.root = Path(root)
110
+ self.records = records
111
+ self.seed = seed
112
+ self.fixed_windows = fixed_windows
113
+ self._zips: dict[str, zipfile.ZipFile] = {}
114
+
115
+ def __len__(self):
116
+ return len(self.records)
117
+
118
+ def _zip(self, shard: str) -> zipfile.ZipFile:
119
+ z = self._zips.get(shard)
120
+ if z is None:
121
+ z = zipfile.ZipFile(self.root / "data" / shard)
122
+ self._zips[shard] = z
123
+ return z
124
+
125
+ def __getitem__(self, index: int):
126
+ rec = self.records[index]
127
+ z = self._zip(rec["shard"])
128
+ rid = rec["id"]
129
+
130
+ def read(ext):
131
+ return z.read(f"{rid}/{rid}{ext}")
132
+
133
+ meta = json.loads(read(".json"))
134
+ warmup = int(meta.get("codes_warmup_frames", 1))
135
+ codes = np.load(io.BytesIO(read(".codes.npy"))) # [T_total, 8] int32
136
+ vae = np.load(io.BytesIO(read(".vae.npy"))) # [F_dav, 128] fp16 frame-major
137
+ probs_z = np.load(io.BytesIO(read(".probs.npz")))
138
+ p_idx, p_val = probs_z["idx"], probs_z["prob"] # [T_total, 8, 50]
139
+
140
+ emitted = codes.shape[0] - warmup
141
+ # window start s needs stitched-timeline latents [stitched_lat(s),
142
+ # stitched_lat(s)+441) and frames [s, s+128) emitted. stitched_lat runs
143
+ # slightly FASTER than uniform (345 per 100 frames), so walk max_start
144
+ # down until its window fits (a handful of iterations at most).
145
+ max_start = emitted - WINDOW
146
+ while max_start >= 0 and stitched_lat(max_start) + LATENTS_PER_WINDOW > vae.shape[0]:
147
+ max_start -= 1
148
+ if max_start < 0:
149
+ # belt-and-braces: manifest filtering should prevent this; fall
150
+ # back to a neighbour rather than killing the DataLoader worker.
151
+ return self[(index + 1) % len(self.records)]
152
+ if self.fixed_windows:
153
+ start = (max_start // 2) if index % 2 else 0
154
+ else:
155
+ g = np.random.default_rng(
156
+ (self.seed * 1_000_003 + index) ^ int.from_bytes(os.urandom(4), "big")
157
+ )
158
+ start = int(g.integers(0, max_start + 1))
159
+
160
+ lat_start = stitched_lat(start)
161
+ latents = vae[lat_start : lat_start + LATENTS_PER_WINDOW].astype(np.float32)
162
+
163
+ bounds = [(start + i) * 441 // 128 for i in range(WINDOW + 1)]
164
+ pool = pool_matrix(bounds) # [128, 441] fp32
165
+
166
+ rows = slice(start + warmup, start + warmup + WINDOW)
167
+ target_codes = codes[rows].astype(np.int64) # [128, 8]
168
+ t_idx = p_idx[rows].astype(np.int64) # [128, 8, 50]
169
+ t_val = p_val[rows].astype(np.float32)
170
+ # Head 0's ids were stored as raw LM token ids (code + 151675) and the
171
+ # uint16 dtype wrapped them mod 65536 -> code + 20603 (verified: own
172
+ # code lands in the unwrapped top-50 for ~100% of frames). Unwrap, and
173
+ # zero out anything outside the semantic vocab (e.g. EOS 151670 -> -5).
174
+ sem = t_idx[:, 0, :] - SEM_TOKEN_WRAP
175
+ bad = (sem < 0) | (sem >= 16384)
176
+ sem[bad] = 0
177
+ t_val[:, 0, :][bad] = 0.0
178
+ t_idx[:, 0, :] = sem
179
+ t_val = t_val / np.clip(t_val.sum(-1, keepdims=True), 1e-8, None)
180
+
181
+ return (
182
+ torch.from_numpy(latents), # [441, 128]
183
+ pool, # [128, 441]
184
+ torch.from_numpy(target_codes),
185
+ torch.from_numpy(t_idx),
186
+ torch.from_numpy(t_val),
187
+ )
188
+
189
+
190
+ def load_manifest(root: str, min_duration_s: float = 8.0) -> list[dict]:
191
+ # min_duration_s: a 128-frame window needs 5.12 s of emitted codes plus
192
+ # stitched-latent margin; 8 s is conservative and drops only 24/8120
193
+ # early-EOS outliers (min in corpus: 3.7 s).
194
+ records = []
195
+ with open(Path(root) / "manifest.jsonl", encoding="utf-8") as fh:
196
+ for line in fh:
197
+ rec = json.loads(line)
198
+ if rec.get("align_ok") and rec.get("probs_ok") and rec.get("duration_s", 0) >= min_duration_s:
199
+ records.append(rec)
200
+ return records
201
+
202
+
203
+ # ----------------------------------------------------------------- model ----
204
+
205
+ def encoder_trunk(model, latents: torch.Tensor, pool: torch.Tensor) -> torch.Tensor:
206
+ """conv stack + pooled transformer -> per-frame hidden [B, 128, d_model]."""
207
+ h = model.conv_in(latents.transpose(1, 2))
208
+ for block in model.blocks:
209
+ h = block(h)
210
+ h = torch.bmm(pool.to(h.dtype), h.transpose(1, 2))
211
+ h = h + model.position[:, : pool.shape[1]].to(h.dtype)
212
+ layers = model.transformer if isinstance(model.transformer, torch.nn.ModuleList) else model.transformer.layers
213
+ for layer in layers:
214
+ h = layer(h)
215
+ return model.norm_out(h)
216
+
217
+
218
+ def depth_teacher_forced(dd, frame_context: torch.Tensor, codes: torch.Tensor) -> list[torch.Tensor]:
219
+ """Parallel teacher-forced depth pass. codes [B, F, 8] ground truth.
220
+ Sequence = [ctx, e0(c0), .., e6(c6)]; causal mask; head i reads position i+1."""
221
+ batch, frames, _ = frame_context.shape
222
+ parts = [dd.context_projection(frame_context).flatten(0, 1).unsqueeze(1)]
223
+ for i in range(7):
224
+ parts.append(dd.prior_embeddings[i](codes[..., i]).flatten(0, 1).unsqueeze(1))
225
+ hidden = dd._decode(torch.cat(parts, dim=1)) # [B*F, 8, D]
226
+ return [dd.heads[i](hidden[:, i + 1]).view(batch, frames, -1) for i in range(7)]
227
+
228
+
229
+ def soft_ce(logits: torch.Tensor, t_idx: torch.Tensor, t_val: torch.Tensor) -> torch.Tensor:
230
+ """-sum p_teacher * log q_student over the teacher's top-50 support."""
231
+ logq = F.log_softmax(logits.float(), dim=-1)
232
+ return -(t_val * logq.gather(-1, t_idx)).sum(-1).mean()
233
+
234
+
235
+ def compute_losses(model, batch, device, acoustic_weight: float, hard_weight: float, log_taus=None):
236
+ latents, pool, codes, t_idx, t_val = (x.to(device, non_blocking=True) for x in batch)
237
+ hidden = encoder_trunk(model, latents, pool)
238
+ sem_logits = model.heads[0](hidden) # [B, F, 16384]
239
+ ac_logits = depth_teacher_forced(model.depth_decoder, hidden, codes)
240
+ if log_taus is not None:
241
+ # Released checkpoints carry uncalibrated (muP-sharp) readouts —
242
+ # harmless for argmax/top-K, fatal for CE. Learnable per-head
243
+ # temperature; folded into head weights at save time.
244
+ taus = log_taus.exp()
245
+ sem_logits = sem_logits / taus[0]
246
+ ac_logits = [ac_logits[i] / taus[i + 1] for i in range(7)]
247
+
248
+ losses = {"sem_soft": soft_ce(sem_logits, t_idx[..., 0, :], t_val[..., 0, :])}
249
+ ac = [soft_ce(ac_logits[i], t_idx[..., i + 1, :], t_val[..., i + 1, :]) for i in range(7)]
250
+ losses["ac_soft"] = torch.stack(ac).mean()
251
+
252
+ if hard_weight > 0:
253
+ losses["sem_hard"] = F.cross_entropy(sem_logits.flatten(0, 1).float(), codes[..., 0].flatten())
254
+ ach = [
255
+ F.cross_entropy(ac_logits[i].flatten(0, 1).float(), codes[..., i + 1].flatten())
256
+ for i in range(7)
257
+ ]
258
+ losses["ac_hard"] = torch.stack(ach).mean()
259
+
260
+ total = losses["sem_soft"] + acoustic_weight * losses["ac_soft"]
261
+ if hard_weight > 0:
262
+ total = total + hard_weight * (losses["sem_hard"] + acoustic_weight * losses["ac_hard"])
263
+ losses["total"] = total
264
+
265
+ with torch.no_grad():
266
+ losses["sem_t1"] = (sem_logits.argmax(-1) == codes[..., 0]).float().mean()
267
+ losses["ac_t1"] = torch.stack(
268
+ [(ac_logits[i].argmax(-1) == codes[..., i + 1]).float().mean() for i in range(7)]
269
+ ).mean()
270
+ return losses
271
+
272
+
273
+ @torch.no_grad()
274
+ def evaluate(model, loader, device, acoustic_weight, autocast_ctx, log_taus=None):
275
+ model.eval()
276
+ sums: dict[str, float] = {}
277
+ n = 0
278
+ for batch in loader:
279
+ with autocast_ctx():
280
+ losses = compute_losses(model, batch, device, acoustic_weight, hard_weight=1.0, log_taus=log_taus)
281
+ for k, v in losses.items():
282
+ sums[k] = sums.get(k, 0.0) + float(v)
283
+ n += 1
284
+ model.train()
285
+ return {k: v / max(n, 1) for k, v in sums.items()}
286
+
287
+
288
+ # ------------------------------------------------------------------ main ----
289
+
290
+ def main():
291
+ ap = argparse.ArgumentParser()
292
+ ap.add_argument("--corpus", default=r"M:\HOT-Step-CPP\_corpora\mm3-rvq-distill-8k")
293
+ ap.add_argument("--encoder-dir", default=r"M:\HOT-Step-CPP\_experiments\open-rvq-pooled-v4")
294
+ ap.add_argument("--encoder-file", default="rvq_encoder.safetensors")
295
+ ap.add_argument("--config-file", default="rvq_encoder_config.json")
296
+ ap.add_argument("--adapter-dir", default=ADAPTER_DIR_DEFAULT)
297
+ ap.add_argument("--out", required=True)
298
+ ap.add_argument("--steps", type=int, default=20000)
299
+ ap.add_argument("--batch", type=int, default=16)
300
+ ap.add_argument("--grad-accum", type=int, default=4)
301
+ ap.add_argument("--lr", type=float, default=1e-4)
302
+ ap.add_argument("--warmup", type=int, default=200)
303
+ ap.add_argument("--weight-decay", type=float, default=0.01)
304
+ ap.add_argument("--acoustic-weight", type=float, default=1.0)
305
+ ap.add_argument("--hard-weight", type=float, default=0.0,
306
+ help="mix in hard-label CE alongside soft distillation")
307
+ ap.add_argument("--sem-tau", type=float, default=16.0,
308
+ help="initial semantic logit temperature (line-searched on pooled-v4)")
309
+ ap.add_argument("--ac-tau", type=float, default=2.0,
310
+ help="initial acoustic logit temperature")
311
+ ap.add_argument("--holdout-frac", type=float, default=0.05)
312
+ ap.add_argument("--eval-every", type=int, default=500)
313
+ ap.add_argument("--save-every", type=int, default=1000)
314
+ ap.add_argument("--workers", type=int, default=2)
315
+ ap.add_argument("--seed", type=int, default=1)
316
+ ap.add_argument("--vram-frac", type=float, default=0.85)
317
+ ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
318
+ ap.add_argument("--smoke", action="store_true", help="2 optimizer steps on CPU-sized batch, then exit")
319
+ args = ap.parse_args()
320
+
321
+ torch.manual_seed(args.seed)
322
+ device = torch.device(args.device)
323
+ if device.type == "cuda":
324
+ torch.cuda.set_per_process_memory_fraction(args.vram_frac)
325
+
326
+ ref = load_adapter_module(args.adapter_dir)
327
+ from safetensors.torch import load_file, save_file
328
+
329
+ enc_dir = Path(args.encoder_dir)
330
+ config = ref.RVQEncoderConfig.from_dict(
331
+ json.loads((enc_dir / args.config_file).read_text(encoding="utf-8"))
332
+ )
333
+ model = ref.MiniMaxMusicRVQEncoder(config)
334
+ model.load_state_dict(load_file(str(enc_dir / args.encoder_file)), strict=True)
335
+ model.to(device).train()
336
+ n_params = sum(p.numel() for p in model.parameters())
337
+ print(f"[model] {n_params/1e6:.1f}M params, warm start from {enc_dir / args.encoder_file}")
338
+
339
+ records = load_manifest(args.corpus)
340
+ train_recs = [r for r in records if not holdout_of(r["id"], args.holdout_frac)]
341
+ hold_recs = [r for r in records if holdout_of(r["id"], args.holdout_frac)]
342
+ print(f"[data] {len(records)} usable records -> {len(train_recs)} train / {len(hold_recs)} holdout")
343
+
344
+ train_ds = Corpus8k(args.corpus, train_recs, args.seed)
345
+ hold_ds = Corpus8k(args.corpus, hold_recs, args.seed, fixed_windows=True)
346
+ loader_kw = dict(
347
+ batch_size=args.batch,
348
+ num_workers=args.workers,
349
+ pin_memory=device.type == "cuda",
350
+ persistent_workers=args.workers > 0,
351
+ )
352
+ train_loader = DataLoader(train_ds, shuffle=True, drop_last=True, **loader_kw)
353
+ hold_loader = DataLoader(hold_ds, shuffle=False, **loader_kw)
354
+
355
+ log_taus = torch.nn.Parameter(
356
+ torch.log(torch.tensor([args.sem_tau] + [args.ac_tau] * 7, dtype=torch.float32, device=device))
357
+ )
358
+ decay, no_decay = [], []
359
+ for name, p in model.named_parameters():
360
+ (no_decay if p.ndim <= 1 or "position" in name else decay).append(p)
361
+ opt = torch.optim.AdamW(
362
+ [
363
+ {"params": decay, "weight_decay": args.weight_decay},
364
+ {"params": no_decay, "weight_decay": 0.0},
365
+ {"params": [log_taus], "weight_decay": 0.0},
366
+ ],
367
+ lr=args.lr, betas=(0.9, 0.95),
368
+ )
369
+
370
+ def lr_at(step):
371
+ if step < args.warmup:
372
+ return args.lr * (step + 1) / args.warmup
373
+ t = (step - args.warmup) / max(args.steps - args.warmup, 1)
374
+ return args.lr * 0.5 * (1 + math.cos(math.pi * min(t, 1.0)))
375
+
376
+ use_bf16 = device.type == "cuda"
377
+
378
+ def autocast_ctx():
379
+ return torch.autocast("cuda", dtype=torch.bfloat16) if use_bf16 else torch.autocast("cpu", enabled=False)
380
+
381
+ out = Path(args.out)
382
+ out.mkdir(parents=True, exist_ok=True)
383
+ shutil.copyfile(enc_dir / args.config_file, out / "rvq_encoder_config.json")
384
+ (out / "run_args.json").write_text(json.dumps(vars(args), indent=1), encoding="utf-8")
385
+ log_path = out / "train_log.jsonl"
386
+
387
+ def save_ckpt(tag: str):
388
+ ck = out / tag
389
+ ck.mkdir(exist_ok=True)
390
+ state = {k: v.detach().to(torch.float32).cpu() for k, v in model.state_dict().items()}
391
+ # Fold the learned temperatures into the readout heads so the saved
392
+ # file is a plain calibrated encoder (argmax/top-K ordering unchanged,
393
+ # CE now meaningful). heads.0 = semantic; depth_decoder.heads.i = c(i+1).
394
+ taus = log_taus.detach().exp().cpu()
395
+ for key in ("heads.0.weight", "heads.0.bias"):
396
+ state[key] = state[key] / taus[0]
397
+ for i in range(7):
398
+ for suffix in ("weight", "bias"):
399
+ key = f"depth_decoder.heads.{i}.{suffix}"
400
+ state[key] = state[key] / taus[i + 1]
401
+ save_file(state, str(ck / "rvq_encoder.safetensors"))
402
+ shutil.copyfile(out / "rvq_encoder_config.json", ck / "rvq_encoder_config.json")
403
+ (ck / "calibration.json").write_text(
404
+ json.dumps({"folded_taus": [round(float(t), 4) for t in taus]}), encoding="utf-8"
405
+ )
406
+ print(f"[ckpt] {ck} (taus folded: {[round(float(t), 2) for t in taus]})")
407
+
408
+ best_hold = float("inf")
409
+ step = 0
410
+ t0 = time.time()
411
+ opt.zero_grad(set_to_none=True)
412
+ data_iter = iter(train_loader)
413
+ while step < args.steps:
414
+ for micro in range(args.grad_accum):
415
+ try:
416
+ batch = next(data_iter)
417
+ except StopIteration:
418
+ data_iter = iter(train_loader)
419
+ batch = next(data_iter)
420
+ with autocast_ctx():
421
+ losses = compute_losses(
422
+ model, batch, device, args.acoustic_weight, args.hard_weight, log_taus=log_taus
423
+ )
424
+ (losses["total"] / args.grad_accum).backward()
425
+ if not torch.isfinite(losses["total"]):
426
+ raise FloatingPointError(f"non-finite loss at step {step}: {losses}")
427
+ torch.nn.utils.clip_grad_norm_(list(model.parameters()) + [log_taus], 1.0)
428
+ for group in opt.param_groups:
429
+ group["lr"] = lr_at(step)
430
+ opt.step()
431
+ opt.zero_grad(set_to_none=True)
432
+ step += 1
433
+
434
+ if step % 25 == 0 or args.smoke:
435
+ row = {k: round(float(v.detach()), 4) for k, v in losses.items()}
436
+ row.update(
437
+ step=step, lr=round(lr_at(step), 8), elapsed_s=round(time.time() - t0, 1),
438
+ taus=[round(float(t), 2) for t in log_taus.detach().exp()],
439
+ )
440
+ print(f"[{step}/{args.steps}] " + " ".join(f"{k}={v}" for k, v in row.items() if k != "step"))
441
+ with open(log_path, "a", encoding="utf-8") as fh:
442
+ fh.write(json.dumps(row) + "\n")
443
+
444
+ if args.smoke and step >= 2:
445
+ print("[smoke] OK — forward/backward/step ran clean")
446
+ return
447
+
448
+ if step % args.eval_every == 0:
449
+ ev = evaluate(model, hold_loader, device, args.acoustic_weight, autocast_ctx, log_taus=log_taus)
450
+ row = {("hold_" + k): round(float(v), 4) for k, v in ev.items()}
451
+ row["step"] = step
452
+ print(f"[eval @{step}] " + " ".join(f"{k}={v}" for k, v in row.items() if k != "step"))
453
+ with open(log_path, "a", encoding="utf-8") as fh:
454
+ fh.write(json.dumps(row) + "\n")
455
+ score = ev["sem_hard"] + args.acoustic_weight * ev["ac_hard"]
456
+ if score < best_hold:
457
+ best_hold = score
458
+ save_ckpt("best_holdout")
459
+
460
+ if step % args.save_every == 0:
461
+ save_ckpt(f"step{step:06d}")
462
+
463
+ save_ckpt("final")
464
+ print(f"[done] {args.steps} steps in {(time.time() - t0)/3600:.2f} h; best holdout hard-CE sum {best_hold:.4f}")
465
+ print("[next] run each kept checkpoint through the REAL-AUDIO gate: "
466
+ "export_codes_v4.py --encoder-file <ckpt>\\rvq_encoder.safetensors "
467
+ "--config-file <ckpt>\\rvq_encoder_config.json, then the 1-step lm_sft_train2 [Base] CE.")
468
+
469
+
470
+ if __name__ == "__main__":
471
+ main()