Skip to content
Closed
2 changes: 2 additions & 0 deletions .agents/issue-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,6 @@ rather than merged. `scripts/check-agent-record.py` gates both.
| [#930](https://github.com/mudler/vllm.cpp/issues/930) | `LTX25-TOKEN-APPEND` | LTX-2.5's phase loop is fixed at one `Ltx2VideoTokenCount(vshape, 1)`, and that single limitation blocks THREE conditioning arms, not the two #930 was written against: reference video, the LAST-frame keyframe, and generated keyframe slots ([#920](https://github.com/mudler/vllm.cpp/issues/920)). Row `LTX25-TOKEN-APPEND` (spec [`ltx25-token-append.md`](specs/ltx25-token-append.md)) ports the two missing halves of the append — `extend_keyframes_mask` (`mask_utils.py:74-105`), which upstream's own docstring says EVERY appending item must call, and `clear_conditioning` (`tools.py:88-117`), which trims back to the target count and restores an ALL-ONES mask rather than the conditioned one — and lifts the last-frame keyframe as the demonstration. The attention mask is NOT the gap and no field is added for it: both ported video items pass a literal `attention_mask=None` (`keyframe_cond.py:68-76`, `reference_video_cond.py:88-96`) and the only route to a non-None mask is `ConditioningItemAttentionStrengthWrapper`, applied solely at `iclora_utils.py:169`. The sigma schedule must keep reading the TARGET count — `math.prod(latent.shape[2:])` (`schedulers.py:32 @ fd4ded7fa`) is the unpatchified target and cannot see an append — so the engine's `Ltx2SigmaSchedule(steps, video.tokens)` call, which sits AFTER the conditioning block (`src/vllm/multimodal/ltx2_video.cpp:1719 @ bc6433d1b`), re-shifts the whole schedule the moment anything appends. Reference video and generated slots stay refused: at `bc6433d1b` the reference refusal's LoRA-metadata cause is still true because PR [#938](https://github.com/mudler/vllm.cpp/pull/938) is open and unmerged | feature |
| [#949](https://github.com/mudler/vllm.cpp/issues/949) | — | Nothing in the tree refuses a borrowed `vt::Tensor` that outlives the object owning its storage, and the ONLY instrument that catches one is `sanitize-cpu`, which is `continue-on-error` — that is how [#904](https://github.com/mudler/vllm.cpp/issues/904) landed. Measured in the #936 review rather than argued: with the #904 fix reverted, a plain Release build with no sanitizer runs the case 18/18 passed, 546 assertions, `rc=0`, because `dtype` lives in the `vt::Tensor` struct and not in the freed buffer, so no ordinary gate can see the dangling read. Three remedies are open and none is foregone: promote the lane once it has a `main` baseline, add a test that fails without a sanitizer, or reject the pattern statically — a prototype detector for a member access chained onto a call returning an owning type by value swept 1777 files with no hit but the defect. Anchors: the owning deleter `src/vllm/model_executor/models/ltx2_device.cpp:1088 @ 800dd082f`, the read `src/vt/cpu/cpu_layernorm.cpp:33 @ 800dd082f`. Listed under `## Owed` in [`ltx2-device-staged-view-uaf.md`](specs/ltx2-device-staged-view-uaf.md) | bug |
| [#933](https://github.com/mudler/vllm.cpp/issues/933) | `ENG-EXPERT-STREAM` | Measure gateability of the `llama-cpp-unsloth` oracle by BUILDING it and RUNNING `Qwen3.8-2.4T-A95B UD-Q1_0` on it. The oracle is pinned at `36fe8e1cc` (branch `iq1-narrow`) and records `gateable = no`, because the IQ1_XXXS port is grounded in the fork's SOURCE, read and cited, which is weaker than a running comparison. It is the only place ggml type 66 is defined: the vllm.cpp pin `237ad9b96` ends at `Q1_0 = 41` and `ggml-org` master `ad1de39e0` at `Q2_0 = 42`, while type 66 carries 96.92 % of that checkpoint's parameters. Running it needs the full 370 GiB checkpoint and, per Unsloth's documentation, at least 450 GB of RAM. Until then the ported arm has no running oracle, which is what `gateable = no` makes visible | task |
| [#953](https://github.com/mudler/vllm.cpp/issues/953) | `MODEL-MUSIC-minimax-music3-mini-max-music3-for-conditional-generation` | `POST /v1/audio/speech` silently DROPPED five keys that SGLang-Omni, serving this same model on this same route, refuses BY NAME: `temperature`, `top_p`, `top_k`, `repetition_penalty` (`request_builders.py:14-19,109-114` — this model's AR stage has ONE sampler, a fixed top-50 draw, `encoders.py:48,94-103`, so the knobs can be neither honoured nor honestly ignored) and `max_new_tokens` (`request_builders.py:56-68` — upstream's LENGTH spelling, counted in 25 Hz FRAMES rather than seconds, so a 250-frame request silently became the family's 60 s default). The identical class as [#925](https://github.com/mudler/vllm.cpp/issues/925), which cost four multi-hour runs. FIXED IN FLOW while sweeping [#672](https://github.com/mudler/vllm.cpp/issues/672) for upstream parity: all five refused by name, RED first in `test_speech_api.cpp`, two mutations both firing | bug |
| [#957](https://github.com/mudler/vllm.cpp/issues/957) | `FIX-OFFLOAD-DOCS-957` | `4a183b731` (#887) turned a configured weight offload from ACCEPTED-AND-INERT into a hard startup refusal, and neither public document followed. `docs/WEIGHT-OFFLOAD.md` still said "a budget you set is accepted, reported, and does not free memory" and `docs/USAGE.md:1473` still said "Accepted and inert today", while `RefuseUnsupportedWeightOffload` (`src/vllm/model_executor/weight_offloader.cpp:72-83` @ 2daa3287f) throws from the load path (`src/vllm/entrypoints/model_loader.cpp:1410-1414` @ 2daa3287f) before any weight I/O. `ModelFactory::supports_weight_offload` defaults false and NO model sets it, so every architecture is refused; `tests/vllm/model_executor/test_weight_offloader.cpp:376-379` @ 2daa3287f asserts that count itself. Found auditing the 28 commits `documentation-checkpoint` flags: 27 needed nothing | bug |
| [#965](https://github.com/mudler/vllm.cpp/issues/965) | `MODEL-MUSIC-minimax-music3-mini-max-music3-for-conditional-generation` | `windows-msvc-cpu`/`windows-msvc-vulkan` fail on EVERY open pull request with `C4456: declaration of 'loaded' hides previous local declaration` at `server_main.cpp:1315` — the speech engine's `loaded` nested inside the text engine's `loaded` at `:1025`, both already on `main`, and the ONLY warning in the job. **It is NOT [#645](https://github.com/mudler/vllm.cpp/issues/645)**, which is the `M_PI` regression in three LTX2 sources: a second cause hiding behind a known-red name, which is why "known-red" needs a MATCHED-ARM check and not a label. Confirmed pre-existing against three unrelated PRs (#956, #950, #939) that all fail identically. Invisible on `main` because `windows-msvc-*` are PR-only ([#584](https://github.com/mudler/vllm.cpp/issues/584)), so it presents to each author in turn as a red their own diff caused. FIXED IN FLOW while landing [#672](https://github.com/mudler/vllm.cpp/issues/672): the inner declaration is renamed, no detector weakened and no warning suppressed | bug |
2 changes: 1 addition & 1 deletion .agents/model-matrix.md

Large diffs are not rendered by default.

190 changes: 190 additions & 0 deletions .agents/specs/minimax-music3.md
Original file line number Diff line number Diff line change
Expand Up @@ -1092,3 +1092,193 @@ do; it is recorded here because the obvious first read of a slow run is "the
language model is slow", and the language model is not the part that is slow —
the LM's own weight load is 180 s of I/O and its forward is 12-14% of the AR
profile.

---

## 10. The parity sweep, the music-only server, and the weights record (#672)

**Developer directive (2026-08-15):** parity on what upstream supports —
"we want to be a good reference" — usage docs for MiniMax-Music3, and in those
docs the models and weights used and supported, the way MiniMax-H3 already does
it. Then, mid-flight: **"we should allow to load only the music model"** and
**"we need to have an e2e test working"**. The first two lines are the scope;
the last two fixed two of its answers as requirements rather than judgements.

### 10.1 The upstream surface, enumerated

SGLang-Omni `748a0b43` at `sglang_omni/models/minimax_music3/` and the diffusers
PR at `c6da9936` were read field by field. What a user can set upstream, and
where each lands here:

| upstream field | upstream default and anchor | here |
|---|---|---|
| `prompt` / `instructions` (the description) | required, `encoders.py:194-198`; SGLang `request_builders.py:104-106` | `description` (alias `prompt`) — **PARITY** |
| `lyrics` / `input` | required, `encoders.py:199-200`; `request_builders.py:103` | `lyrics` — **PARITY** |
| `audio_duration` | 60.0 s, `encoders.py:251-259` | `audio_duration` (alias `duration`) — **PARITY**, same default |
| `num_inference_steps` | 30, `denoise.py:141-148` | `num_inference_steps` — **PARITY**, same default |
| CFG scale | **not a request field** — frozen at 1.7 into the guider component, `denoise.py:180`; a serve-time knob `dit_cfg_scale` in SGLang, `stages.py:76-95` | `guidance_scale`, a real per-request control defaulting to 1.7. **AHEAD of both arms** |
| `generator` / `seed` | a `torch.Generator` in diffusers (`encoders.py:260`, `denoise.py:111`); an integer defaulting to 0 in SGLang (`payload_types.py:25`) | `seed`, integer, default 0 — **PARITY** with the SGLang spelling |
| `max_new_tokens` (frames) | 9000 cap, `request_builders.py:56-68` | **REFUSED BY NAME**, pointing at `audio_duration` and the /25 conversion |
| `temperature`, `top_p`, `top_k`, `repetition_penalty` | **refused** by upstream, `request_builders.py:14-19,109-114` | **REFUSED BY NAME** — was SILENT, and that silence was the #925 class |
| `voice`, `speed` | refused, `request_builders.py:83-92` | refused — **PARITY** |
| `stream` | refused, `request_builders.py:115-116`; `supports_streaming_vocoder=False` | refused — **PARITY**. Upstream has no streaming in either arm |
| `response_format` | wav/mp3/flac/pcm/aac/opus, `protocol.py:291` | `"wav"` only — **OWED**, no encoder is vendored. Note upstream **downmixes to mono** for any non-wav format (`client/audio.py:328-334`) |
| prompt ceiling 5000 tokens | `encoders.py:42,212-215` | enforced, `minimax_music3_ar.cpp:226` — **PARITY** |
| frame ceiling 9000 | diffusers **CLAMPS** silently (`encoders.py:287`); SGLang **REJECTS** (`request_builders.py:64-67`) | we CLAMP, mirroring the primary oracle. Gated at 360 s and 3600 s |
| output rate | diffusers 44100, no resample; SGLang resamples to 32000 (`acoustic.py:55-58,423`) | 44100 native — the §1.1 decision. The 32 kHz delivery transform stays **OWED** |
| N samples per request | **neither arm supports it** (`denoise.py:117-122` is batch 1; no `n` field on `protocol.py:334-368`) | one waveform per request — **PARITY** |
| N concurrent requests batched | SGLang only: continuous batching at 16, **two engine rows per request** for the CFG twin (`engine_builder.py:74-77`), plus `POST /v1/audio/speech/batch` (`openai_api.py:1277`) | we serialize per engine handle — **OWED** |
| `sgl-omni serve --model <music-model>` and nothing else | the norm: the pipeline is three stages with no chat LLM, `models/minimax_music3/config.py:29-63` | **CLOSED** — see §10.2 |

**Closed by this change:** the music-only server, the missing example, the four
sampling refusals, the `max_new_tokens` refusal.
**Refused by name and recorded as owed:** the non-wav response formats, request
batching and the `/batch` route, the 32 kHz delivery resample, the native `.pth`
arm, streaming (which upstream does not have either, so it is a permanent
refusal rather than a debt).

### 10.2 `--model` is optional when `--speech-model` is given

Serving a 28.5 GB music model also forced loading an unrelated text model,
because `--model <dir>` was unconditionally required. On this box the smallest
available text checkpoint is 35B, so **the recipe this project documented was
effectively unrunnable**, and upstream's own is `sgl-omni serve --model
MiniMaxAI/MiniMax-Music3` with no text tower anywhere.

`--speech-model` alone now loads only the speech engine and registers only
`/v1/audio/speech`. It is the third instance of a shape already in
`server_main.cpp` — a pooling checkpoint serves `/v1/embeddings` alone, a
Parakeet checkpoint serves `/v1/audio/transcriptions` alone — and it mirrors
vLLM's task-conditional registration (`api_server.py:255-265`).

**It is ADDITIVE and that is proved, not argued.** The only case whose verdict
changes is `--model` absent *and* `--speech-model` absent, which was an error
and remains one, with a message that now names both ways to satisfy it.
`--model` alone and `--model` + `--speech-model` take byte-identical paths.

The route table is gated **in both directions over a real socket**, because a
handler-dispatch test cannot see route registration at all: with no synthesizer
`/v1/audio/speech` is a 404 from the route table with no envelope leaked, and on
a speech-only server `/v1/completions` and `/v1/chat/completions` are 404 while
`/v1/audio/speech` returns `audio/wav`.

### 10.3 The e2e gate: what it examined, reported rather than implied

The gate reported `test cases: 5 | 5 passed` and **`assertions: 0`** whenever the
checkpoint was absent. Five green case names over an empty run — the same shape
that fooled this project on `test_qwen3_paged_engine`, which "passes 2/2" while
asserting nothing because its snapshots are dgx-only.

The file is now split. **The checkpoint-free half runs unconditionally in CI**:
the request contract on the exact body the real case posts, the near-miss
refusals, the duration arithmetic including both ceilings, and the speech-only
route table over a real socket with a stub synthesizer. `assertions: 0` is
therefore structurally impossible. **The checkpoint half** keeps its env gate,
and the real case now runs over a real socket against the music-only server
shape rather than calling `handle_audio_speech` directly.

A **coverage-report case** prints, every run, which arms ran and why any did not.
Its assertion deliberately is **not** a cross-case counter: `-tc="…COVERAGE…"`
runs it alone, the counter is legitimately zero, and a gate that reds for the way
it was invoked is a gate somebody deletes. It asserts a cheap fact about the
checkpoint itself instead — 44100 Hz, hop 512, vocab 200000, 8 codebooks, read
from the component `config.json` files in milliseconds — which holds under any
invocation.

**All three arms, measured on this box 2026-08-15**, so the difference between
them is visible rather than asserted:

| arm | cases | assertions | what ran |
|---|---|---|---|
| no env vars | 9 | **37** | the checkpoint-free half only. Was 5 / **0** |
| `VLLM_CPP_MUSIC3_CHECKPOINT` | 9 | **86** | + decode, WAV and condition-mix; `checkpoint_arms_run=3` |
| + `VLLM_CPP_MUSIC3_DIT=1` | 9 | **582** | + the full tail and the music-only server over a real socket; `checkpoint_arms_run=5` |

The full arm's own numbers: `POST /v1/audio/speech -> 200 audio/wav, 12332 bytes
in 518.0 s wall`; 2 AR frames -> 6 latent frames -> 3072 samples per channel
(0.0697 s); 6144 int16 samples, all non-zero, 0 clipped, 2818 of 3072 positions
differing between left and right; and `/v1/completions` and
`/v1/chat/completions` both 404 from the route table, which is the music-only
claim made over the wire against the real 28.5 GB engine rather than a stub.

### 10.4 The weights are documented (porting-a-model.md §2.1)

`docs/USAGE.md` carries the tables the H3 sections already carried, one row per
artifact, with the repo **and revision**: the diffusers arm at
`MiniMaxAI/MiniMax-Music3` @ `fbdf52fbaaca799592917417eb05f1899f1255ec`,
component by component, **28.5 GB resident** (28 517 617 303 B, measured) out of
a 57.4 GB repository and why the two differ; the native `.pth` arm we refuse and
that SGLang-Omni serves; the one implemented GGUF Q4_K artifact with its sha256;
and the fourteen third-party quantized repositories in five formats, each marked
refused and each marked third-party.

The revision is **verified rather than copied**:
`condition_encoder/diffusion_pytorch_model.safetensors` on disk hashes to
`83179c5eaa9a68a370affe0c1b96c2179f659ea4175666b31071490a202c2a4d`, which is that
revision's own LFS record for the file.

### 10.5 The first sample a human can hear, and where it is not

**2.0 s of 44100 Hz stereo, from this engine, in 3286 s of wall clock.** The
e2e gate's own artifact is 0.07 s — the shortest request that still enters every
stage — which nobody can listen to. `minimax-music3-gen` at `--duration 2.0
--steps 2 --seed 7` produced 88 064 frames per channel: RMS 0.03169, peak
0.97437 full-scale with **0 clipped samples**, 175 858 of 176 128 int16 samples
non-zero, and 84 073 of 88 064 positions differing between left and right, so
the 128 latent channels are folded into two streams of 64 rather than
interleaved. Verified independently of the generator, by re-reading the RIFF
file.

x86 20-core CPU, load average swinging 7 to 150 across the run (several other
sessions on the box), 17.8 GB resident. No speed claim is made or implied: the
acoustic half is upstream's own fp32 and the depth decoder and DiT are scalar
host loops by construction (see `## Now`).

**Its samples are compared to nothing, and that is structural rather than an
omission.** §5 withdrew the token gate; §6/W6 records that a request's waveform
can never equal `waveform.npy` because both the codes and the initial latents
are seeded random draws. The clip demonstrates the pipeline runs and emits a
well-formed, non-silent, non-clipped, genuinely stereo signal. The per-stage
gates are what speak to correctness.

**It is NOT committed, and the reason is a checker rather than a preference.**
`scripts/check-pr-size.py` classifies every repository path; `ASSET` accepts
`assets/*.{png,svg}`, `BENCH_EVIDENCE` accepts
`benchmarks/{demo,media}/*.{json,png,gif,mp4,log}`, and neither takes a `.wav`.
The only classified home for one is under `tests/`, where a file compared to
nothing would sit beside the oracle goldens and imply it was one — which
`test_minimax_music3_e2e_real.cpp` explicitly refuses for its own artifact
("under the build tree, never under tests/ — no golden is created, replaced or
implied by this"). Widening either pattern would be widening a checker's scope
to make a change pass, which AGENTS.md forbids without its own spec and
red-before evidence, and this clip does not justify one. Regenerating it is one
command.

### 10.6 A red that belonged to nobody, found by checking a matched arm (#965)

`windows-msvc-cpu` and `windows-msvc-vulkan` failed on this row's pull request.
Both are habitually red and both are habitually attributed to
[#645](https://github.com/mudler/vllm.cpp/issues/645). **They were not #645.**
#645 is the `M_PI` portability regression in three LTX2 sources; this was:

```
server_main.cpp(1315,55): error C2220: the following warning is treated as an error
server_main.cpp(1315,55): warning C4456: declaration of 'loaded' hides previous local declaration
```

— W6's own speech-attach block declaring `loaded` inside the scope of the text
engine's `loaded` at `:1025`. The only warning in the job, and on `main` since
W6 landed.

**What found it was the matched-arm check, not the label.** Three unrelated open
pull requests — #956, #950, #939, none touching the speech surface — fail with
the identical `C4456`. That is what separates "pre-existing" from "mine", and it
is the step that a known-red list invites you to skip. Because `windows-msvc-*`
are PR-only ([#584](https://github.com/mudler/vllm.cpp/issues/584)), `main`
carries no baseline, so the failure presents to every author in turn as a red
their own diff caused — and a second cause sitting behind a known one is
invisible for exactly as long as nobody reads the log.

Fixed in flow: the inner declaration is renamed, with a comment saying why the
name is not `loaded`. No detector weakened, no warning suppressed, no behaviour
changed.
Loading
Loading