Skip to content

feat(MUSIC3-DEPTH-DEVICE): the RVQ depth decoder reaches the device at bf16, and the measurement moves the blocker off the dtype (#672, #1309) - #1330

Merged
localai-bot merged 6 commits into
mainfrom
row/MUSIC3-DEPTH-DEVICE
Aug 19, 2026
Merged

feat(MUSIC3-DEPTH-DEVICE): the RVQ depth decoder reaches the device at bf16, and the measurement moves the blocker off the dtype (#672, #1309)#1330
localai-bot merged 6 commits into
mainfrom
row/MUSIC3-DEPTH-DEVICE

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Row MUSIC3-DEPTH-DEVICE, issues #1309
and #672; spec
.agents/specs/minimax-music3.md §19.

MiniMax-Music3's 0.646 B RVQ depth decoder was 48.4 % of a run on
thor:gpu0 -- 78.1 s of 161 s, the largest single term -- so a 0.646 B model
cost 6.3x the 8.6 B language model beside it on the same box, and the reason
was which processor each ran on. §16.4 measured the host kernel at ~4 bytes of
f32 weight traffic per multiply-accumulate: 2.28 GB per call, 8 calls a frame.
This row moves that sweep to the device and halves it. It is §11.4's last owed
device row, and §14.5 blocked it on a dtype rather than on the work.

The dtype, which is what the row was blocked on

The oracle settles it by declaring nothing. MiniMaxMusic3RVQDepthDecoder takes
no dtype parameter and contains no torch.float32 literal and no .float()
call (diffusers @ c6da9936,
minimax_music3_rvq_depth_decoder.py:101-125); its single cast is the down-cast
:51 .to(query.dtype). The dtype is imposed by load_components(dtype=...),
and tools/oracle/music3_oracle.py resolves rvq_depth_decoder: torch.bfloat16
under both policies. So: bf16 storage, f32 accumulation, which is exactly the
contract vt::MatmulBT has carried all along. §14.5's objection -- that an f32
vt::MatmulBT would drop the bf16 rounding every gated number was taken with --
is answered by using the bf16 one.

The narrowing is lossless rather than a rounding we tolerate: AtRuntimeDtype
already rounds every AR-half tensor through bf16 into an f32 carrier, so
vt::F32ToBF16 on those values is the exact inverse of the widening the loader
did. A finding falls out that no gate here can see, and it is recorded rather
than fixed: the host arm keeps bf16-exact weights in std::vector<float>, so
every golden, token gate and WAV hash passes while that path moves twice the
oracle's bytes (§19.2a).

No new kernel

Five shared ops, each already carrying a CPU and a CUDA provider:
vt::RmsNorm, vt::MatmulBT, vt::AttentionCross, vt::SiluAndMul, vt::Add.
Two merges the seam asks for: to_q | to_k | to_v as one [3H, H], and
gate_proj | up_proj as one [2I, H] consumed through
layers::UnquantizedMlpGateUpMethod rather than a hand-rolled equivalent.

The attention is causal upstream and vt::AttentionCross is still the right op,
by an identity rather than a shortcut: the call presents one query row against
a cache of positions that are all at or before it, so a causal mask masks nothing.

What is measured, and what is not claimed

No parity claim and no speed number. The decisive gate is
test_minimax_music3_ar_real run with the device arm, and it needs the 28.5 GB
checkpoint; until it runs, AGENTS.md forbids quoting a Thor A/B. The A/B is
blocked on correctness, not on the box.

The four review findings, repaired

A fresh review returned FAIL. The port's algebra was not one of the findings --
the reviewer independently confirmed it exact.

The reachability leg this row cites #1131 for was open one level up

Two selections exist. The inner one, the append lambda, was gated and red. The
outer one, the engine's if (queue_.device.type != kCPU) { ... }, was not:
deleting the whole block -- the only thing --speech-device 1 reaches -- left
test_minimax_music3_ar 35/35 and test_minimax_music3_speech 9/9 green. That is
#1131's shape reproduced by the change that names #1131 as its reason for
existing, and it is structural rather than careless: on a CPU-only runner that
condition can never be true.

The rule now lives in Music3SelectDepthArm, which runs on both sides of the
condition and is therefore drivable from a CPU gate. The gate enters it with a CPU
queue -- which must stage nothing, keep the host arm, and release nothing even at
release_host=true -- and with a fabricated queue on each of five non-CPU device
types, where the invariant is that the selection engages or refuses by name.
The third outcome, a quiet fall-back to the host loop, is the defect. kCUDA is
excluded from that list deliberately: on a CUDA build without a device the staging
would fail inside the CUDA runtime, and a call designed to fail latches a sticky
error that the next unrelated kernel reports as its own.

What this does not close is stated rather than implied. Deleting the engine's
two-line call still leaves both suites green, because the engine needs the
28.5 GB checkpoint and a real device. §19.7 carries that as owed, naming row
MUSIC3-DEPTH-DEVICE and #1131, and §19.5 records the mutation with the binary
hashes that prove it was not a stale-binary artefact.

The tolerance was fitted to one seed

Changing only the RNG seed of the reference weights -- same distribution, same
geometry, no defect -- reds the shipped mean <= 4.0 on three of six equally
valid draws. Worse, worst <= 512 could not discriminate at all: a correct arm
reads worst 7340 at 0xDEADBEEF while the gate/up half swap reads 6641 and the
wrong attention scale 6865, so any worst bound loose enough to admit a correct
implementation admits two structural defects.

The case now measures six seeds and gates the median, which is exactly
1 bf16 ULP at every seed for the correct arm against a tightest defect of 3 --
a constant, not a distribution, so a redraw cannot move it. The mean stays gated
at 15 inside a (9.904, 19.6) window that §19.4b states rather than hides.
worst is reported and carries only a non-finite canary.

Six seeds also exposed a defect in the metric itself: a reference value of exactly
zero has no ULP -- bf16's spacing there is the denormal floor -- so one seed read
mean 8.8e32 with the arm correct. Zero references now go in their own bucket,
measured absolutely, and the two counts are asserted to sum, so a defect
cannot hide by growing the un-gated bucket.

The tolerance was blind to a too-wide dtype, on a row whose thesis is a dtype

#1131 offers two instruments -- "invocation count or resident dtype" -- and
this row had taken only the first. Widening one activation buffer to kF32 left
every case green while the path moved twice the bytes: AGENTS.md's "a token gate
cannot detect a dtype that is too wide" landing on this row exactly, and §19.2a's
own finding about the host arm arriving inside the arm that exists to fix it.
Music3DepthDeviceResidentDtypes() now reports a bit per dtype over every buffer
the forward makes resident, read back by the gate rather than restated there.

A stale upstream anchor

normalization.py:600-606 was wrong by ~46 lines and landed on
GlobalResponseNorm. At the pinned diffusers c6da9936 the two roundings are
normalization.py::RMSNorm.forward:557-561, in the class the decoder actually
constructs (minimax_music3_rvq_depth_decoder.py:78,80,122). Corrected in the
header and both spec sites, with the symbol named. The copy in the append-only
.agents/issue-index.md stays.

A correction that outlives this row

While repairing the above, §19.4's own reason 3 turned out to be wrong. It said
vt::RmsNorm "deliberately drops a rounding" and that "the shared op is wider
than the reference". Verified directly at the parity pin 555967922:

  • csrc/cpu/layernorm.cpp computes fp32_out = fp32_x * fp32_s_variance * fp32_w
    and narrows once at scalar_vec_t out(fp32_out);
  • csrc/libtorch_stable/layernorm_kernels.cu:93 computes
    static_cast<scalar_t>(x * s_variance * w);
  • upstream landed the weight-dtype multiply as vllm#42379 and reverted it as
    vllm#46070, which is an ancestor of the pin.

So vt::RmsNorm mirrors its reference exactly. The mistake was generalising a
diffusers observation onto a shared op: AGENTS.md makes vLLM the only
reference wherever it implements the behaviour, and it implements RMSNorm, so
diffusers was never the mirror source for the op -- only for the depth decoder
module, which is what the host arm mirrors. Both arms are correct against the
reference each answers to, and this term of the band therefore does not close.
#1322's surviving half is vt::SiluAndMul, where F.silu(gate) really does
produce a bf16 tensor before the multiply.

The mutation battery

Every run on a clean tree, with the compiler exit status, git diff --stat and
the binary sha256 printed, and the source restored and verified afterwards.

mutation result
gate/up half swap RED 2 cases / 14 assertions
q|k|v merge order swapped RED 2 / 15
wrong attention scale RED 2 / 13
dropped position embedding RED 2 / 15
K/V cache row collision RED 2 / 14
activation buffer bf16 -> f32 RED 1 / 1
K/V cache buffer bf16 -> f32 RED 1 / 1
Music3SelectDepthArm gutted RED 1 / 6
engine's call to the selector deleted GREEN -- the residual, owed above

Gate at the head of this branch: test_minimax_music3_ar 37 cases / 640
assertions SUCCESS, test_minimax_music3_speech 9 / 223 SUCCESS,
check-fusion-consistency.py rc=0 over 16 glue TUs and 12 gated-MLP TUs.
scripts/agent-preflight.sh --staged is green except
test_cpu_x86_llamacpp_floor, which is the load-dependent #618 red -- this diff
touches no scripts/ file and the box was at loadavg 251.

Two smaller things, and one filed

The K/V cache now draws from the device pool through DBuf rather than
backend.Alloc/Free. It is a local of Music3DepthStage, so it is built and
destroyed once per frame, and 16 raw cudaMalloc plus 16 synchronizing
cudaFree per frame is what the pool exists to avoid. No speed number is
quoted
: it is unmeasured, and the A/B is blocked on correctness.

#1351 is filed rather than fixed: check-fusion-consistency.py Check 2 runs
_MERGED_GEMM_SEAM over path.read_text(), so a comment mentioning
MlpGateUpMethod exempts a TU from the check. Reproduced by calling the checker's
own functions -- the hand-rolled variant with comments kept reads
uses_merged_gemm_seam = True, and flips to False only once comments are
stripped. A checker change needs its own spec, a red-before test and green-after
evidence, and widening the regex is what AGENTS.md forbids.

One record collision, resolved and worth knowing about

Two rows claimed ## 17 of .agents/specs/minimax-music3.md at the same time.
#1315's request-key contract landed there first, so it keeps the number and this
row's section renumbers to §19 -- not 18, because the open #1356
(MUSIC3-VOCODER-CONV-SPEED) already claims 18 on its branch. Resolved by taking
main's section whole and re-applying this row's beside it, never by a three-way
merge; main's own back-references and every §17 in multimodal-speed.md,
kimi-linear.md and benchmark-record.md are untouched, as are the index rows
for #1315, #1336 and #1337.

The append-at-the-tail shape of that spec cannot detect this collision until the
two branches meet, which is worth recording: a section number is a shared key in
a file that has no lock.

What is still owed

The projection, audio embeddings, audio heads, CFG mix and top-k draw stay on the
host -- ~1.6 % of the stage today, and the stage's remainder once the forward
moves. The CUDA kernels themselves are unreached by any gate here, and so is the
engine's own call to Music3SelectDepthArm: both need thor:gpu0 and the 28.5 GB
checkpoint, and §19.6 records them with #1131 owning the switch leg. The DiT arm
one block below still carries in full the untestable shape this row just removed
from the depth arm, and #1131 still owns that too.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]

mudler added 2 commits August 19, 2026 07:59
…e arm was blocked on, and record what the measurement refuted (#672, #1309, #1322)

`ar.depth_forward` is 48.4% of a MiniMax-Music3 run on `thor:gpu0` and the last
large stage still on the host, so a 0.646B decoder costs 6.3x the 8.6B language
model beside it. This is §11.4's last owed device row; the vocoder closed in §13
and the DiT in §14.

§14.5 blocked it on a dtype rather than on the work: routing an
`ArCompute::kBFloat16` decoder through an f32 `vt::MatmulBT` would silently drop
the rounding every gated number was taken with. That reasoning is right and its
conclusion has expired, because the row needs the bf16 `vt::MatmulBT`, which the
op has always been.

The oracle settles the dtype by declaring none. `MiniMaxMusic3RVQDepthDecoder`
takes no `dtype` parameter and carries no `torch.float32` literal and no
`.float()` call; its single cast is the down-cast `.to(query.dtype)`. The dtype
comes from `load_components`, and `tools/oracle/music3_oracle.py` resolves the
depth decoder to bf16 under both of its policies. The two fp32 promotions in the
depth loop sit after the decoder, on logits, and already run on the host.

§17.4 then predicted three sources for the arm's divergence and led with the
accumulator. §17.4a records that the measurement puts it LAST: ~97% is rounding
POLARITY inside two shared ops, and with those aligned the composed stage is
BIT-IDENTICAL. So the blocker was never the dtype, and it is not the dtype now —
it is that the seam cannot express the reference's rounding, which is #1322 and
its own row.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…t bf16, on five shared ops and no new kernel (#672, #1309)

The arm §17 specifies. Weights and activations bf16 with f32 accumulation, which
is `vt::MatmulBT`'s contract unmodified. The narrowing is exact rather than
tolerated: `AtRuntimeDtype` already rounds every AR-half weight through bf16 into
its f32 carrier and `Store(..., kBFloat16)` does the same to every activation
crossing this boundary, so `F32ToBF16` here is the inverse of the loader's
widening, value for value.

No new kernel. `vt::MatmulBT`, `vt::RmsNorm`, `vt::AttentionCross`,
`vt::SiluAndMul` and `vt::Add` all carry a CPU AND a CUDA provider, and the MLP's
gate/up pair routes through `layers::UnquantizedMlpGateUpMethod` — the shared
merged-GEMM seam — rather than a hand-rolled equivalent, so a quantized arm can
inherit a fused scheme. `to_q|to_k|to_v` stage as one [3H, H] sweep.
`vt::AttentionCross` is the right op by an identity: one query row against a
cache of positions all at or before it, so a causal mask masks nothing.

Because every op has a CPU provider, the gate drives the PRODUCTION
`Music3DepthStage` on a CPU queue with no GPU and no checkpoint, and asserts
WHICH ARM RAN through `Music3DepthDeviceForwardCount()` rather than inferring it
from numbers the two arms agree on by design. Deleting the production device
branch reds 1 case / 1 assertion. That is the leg #1131 records as missing for
the DiT arm, and not repeating it is why the counter exists.

What is NOT claimed: parity, and any throughput number. The numeric bound bounds
the divergence §17.4a attributes; it does not prove agreement with the reference.
The decisive gate is `test_minimax_music3_ar_real` with the device arm, it needs
the 28.5 GB checkpoint, and it has not been run — so the Thor A/B is blocked on
correctness rather than on the box.

`docs/USAGE.md` moves the depth decoder from host to device in its stage table
and says what the dtype is, why the narrowing is lossless, what does not move
with it, and that no throughput number is quoted for it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
mudler added 4 commits August 19, 2026 12:06
The row branch was cut from f06b9e9. origin/main has since taken the
CUDA-graph persistent step input and the record-anchor ratchet repair, neither of
which touches any MiniMax-Music3 file, so the merge is clean and the row's gate
is unaffected. Merged rather than rebased because the reviewed head 9dd5199 is
published on #1330, and rewriting it would reset the pull request's CI approval.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…re-place the tolerance on six seeds, and correct the RmsNorm divergence (#1309, #1131, #1351)

A fresh review returned FAIL on four things. The port's algebra was not one of
them -- the reviewer independently confirmed it exact -- and these are the
repairs, plus one correction that is larger than the row.

THE SELECTION. `Music3SelectDepthArm` replaces the engine's
`if (queue_.device.type != kCPU) { ... }`. That condition is false on every
runner CI owns, so a branch written at that line is the one line no gate can
enter, which is #1131's shape reproduced by the change that names #1131 as its
reason for existing. The review proved it: deleting the whole block left
`test_minimax_music3_ar` 35/35 and `test_minimax_music3_speech` 9/9 green. The
rule now lives in a function that runs on BOTH sides of the condition, so the
gate drives it with a CPU queue -- which must stage nothing, keep the host arm
and release nothing even at `release_host=true` -- and with a fabricated queue on
each of five non-CPU device types, where the invariant is that the selection
ENGAGES or REFUSES BY NAME. The third outcome, a quiet fall-back to the host
loop, is the defect. Gutting the selector reds 1 case / 6 assertions. `kCUDA` is
left out of that list deliberately: on a CUDA build without a device the staging
would fail inside the CUDA runtime, and a call designed to fail latches a sticky
error the next unrelated kernel reports as its own.

What this does NOT close is stated in the spec rather than implied: deleting the
engine's two-line CALL still leaves both suites green, because the engine needs
the 28.5 GB checkpoint and a real device. §17.7 carries that as owed, naming row
`MUSIC3-DEPTH-DEVICE` and #1131.

THE TOLERANCE was fitted to one draw. Changing only the RNG seed of the
reference weights reds the shipped `mean <= 4.0` on three of six equally valid
seeds. Worse, `worst <= 512` cannot discriminate at all: a CORRECT arm reads
worst 7340 at `0xDEADBEEF` while the gate/up half swap reads 6641 and the wrong
attention scale 6865, so any `worst` bound admitting a correct implementation
admits two structural defects. The case now measures six seeds and gates the
MEDIAN, which is exactly 1 bf16 ULP at every seed against a tightest defect of 3.
The mean stays gated at 15, inside a (9.904, 19.6) window that the spec states
rather than hides. Five structural mutations red at 2 cases / 13-15 assertions
each.

Six seeds also exposed a defect in the metric. A reference value of exactly zero
has no ULP, so one seed read mean 8.8e32 with the arm correct. Zero references
now go in their own bucket, measured absolutely, and the two counts are asserted
to SUM.

THE RESIDENT DTYPE. #1131 offers two instruments, "invocation count OR resident
dtype", and this row had taken only the first. The review widened one activation
buffer to `kF32` and every case stayed green while the path moved twice the
bytes. `Music3DepthDeviceResidentDtypes()` now reports a bit per dtype over every
buffer the forward makes resident, read back by the gate rather than restated
there. That mutation and the same widening of the K/V cache each red 1 case / 1
assertion.

THE RmsNorm DIVERGENCE WAS MISDESCRIBED, and the correction outlives this row.
§17.4 said `vt::RmsNorm` "drops a rounding" and is "WIDER than the reference".
Verified at the parity pin `555967922`: `csrc/cpu/layernorm.cpp` computes
`fp32_out = fp32_x * fp32_s_variance * fp32_w` and
`csrc/libtorch_stable/layernorm_kernels.cu:93` computes
`static_cast<scalar_t>(x * s_variance * w)`. Upstream landed the weight-dtype
multiply as vllm#42379 and REVERTED it as vllm#46070, an ancestor of the pin. So
`vt::RmsNorm` mirrors its reference exactly. The mistake was generalising a
diffusers observation onto a shared op: AGENTS.md makes vLLM the only reference
wherever it implements the behaviour, and it implements RMSNorm, so diffusers was
never the mirror source for the op -- only for the depth decoder MODULE, which is
what the HOST arm mirrors. Both arms are correct against the reference each
answers to, and this term therefore does not close. #1322's surviving half is
`vt::SiluAndMul`.

Two smaller things. The K/V cache now draws from the device pool through `DBuf`
rather than `backend.Alloc`/`Free`: it is a local of `Music3DepthStage`, so it is
built and destroyed once per FRAME, and 16 raw `cudaMalloc` plus 16 synchronizing
`cudaFree` per frame is what the pool exists to avoid. No speed number is quoted
-- it is unmeasured, and the A/B is blocked on correctness. And #1351 is filed
rather than fixed: `check-fusion-consistency.py` Check 2 runs its regex over
`path.read_text()`, so a COMMENT mentioning `MlpGateUpMethod` exempts a TU from
the check; a checker change needs its own spec and red-before evidence.

The upstream anchor `normalization.py:600-606` was wrong by ~46 lines and landed
on `GlobalResponseNorm`. It is `normalization.py::RMSNorm.forward:557-561`, in
the class the decoder constructs. The copy in the append-only issue index stays.

`docs/USAGE.md` drops the single-seed numbers it quoted as if they were the arm's
deviation, states what is and is not gated, and stops calling the RmsNorm term a
shortcoming of the shared op.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
`origin/main` took the ENG-EXPERT-STREAM-DEVICE scoping spec while this row's
review repairs were in flight. It touches no MiniMax-Music3 file and no file this
row's gate reads. The only shared path is the append-only
`.agents/issue-index.md`, where both sides append and neither edits, which is
what the `merge=union` driver on that path exists for; the appended rows are
verified present exactly once below the merge.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…-> 19

Two rows claimed `## 17` of `.agents/specs/minimax-music3.md` concurrently, which
the append-at-the-tail shape of that file cannot detect until they meet. #1315's
request-key contract LANDED there first, so it keeps the number, and this row's
depth-decoder section renumbers. It becomes 19 rather than 18 because the open
#1356 (`MUSIC3-VOCODER-CONV-SPEED`) already claims 18 on its branch; taking 18
here would have produced exactly the same collision one merge later.

Resolved by taking main's section whole and re-applying this row's edit beside
it, never by a three-way merge of the two. Main's own §17.4/§17.7 back-references
at :3526, :3606 and :3670 point INSIDE its section and are untouched. The 20
references to this row's section in the header, the two model TUs, the test and
the three not-yet-landed issue-index rows all move to §19; the index rows for
#1315, #1336 and #1337 are main's and are untouched, as is every §17 in
`multimodal-speed.md`, `kimi-linear.md` and `benchmark-record.md`, which are
those specs' own section 17.

Nothing else in the incoming range touches a MiniMax-Music3 file, so the row's
measured evidence stands as taken.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants