Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/issue-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,6 @@ rather than merged. `scripts/check-agent-record.py` gates both.
| [#810](https://github.com/mudler/vllm.cpp/issues/810) | `MODEL-TEXT-nemotron-h-nemotron-hfor-causal-lm` | NemotronH is not reachable through `include/vllm.h`: `GPUModelRunner::initialize_kv_cache` rebuilds the RECURRENT half of the allocation from `config_.linear_*` instead of the `MambaSpec` the model published, so every non-Qwen3.5 hybrid is refused by Qwen3.5's name at `runner.cpp:525`, and per-layer membership comes from `config_.layer_types[l] == "linear_attention"` rather than `KVCacheGroupSpec::layer_names`. Spec [`nemotron-h-abi-e2e.md`](specs/nemotron-h-abi-e2e.md); the attention half at `runner.cpp:539-607` is already spec-driven and is the model to mirror. Note the SAFETY constraint recorded there: neutering the check alone reaches a forward that ignores `attn_kv`/`gdn_state`/`num_reqs`, which is strictly more dangerous than the refusal | bug |
| [#873](https://github.com/mudler/vllm.cpp/issues/873) | `GATE-CI-CONCURRENCY` | `main` went RED on six release/registration gates after the #865 `ci.yml` rewrite: `check-release-binary-contract.py` and `check-test-registration.py` credit a checker to CI only through an UNCONDITIONAL job, and #865 gave `agent-record` an `if:`; the closed-PR skip is re-expressed through `needs:` and the byte-exact Windows PR proof schema restored (spec [`ci-concurrency.md`](specs/ci-concurrency.md)) | bug |
| [#874](https://github.com/mudler/vllm.cpp/issues/874) | — | `windows-msvc-cpu`/`windows-msvc-vulkan` still start on a CLOSED pull request: `check-release-workflow.py::validate_pr_ci` compares their whole job mapping for equality, so neither an `if:` clause nor a `needs:` guard can be added. Listed under `## Owed` in [`ci-concurrency.md`](specs/ci-concurrency.md) | bug |
| [#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 |
| [#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 |
2 changes: 1 addition & 1 deletion .agents/model-matrix.md

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions .agents/specs/minimax-music3.md
Original file line number Diff line number Diff line change
Expand Up @@ -1092,3 +1092,148 @@ 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.

### 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.
2 changes: 1 addition & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ on the committed fixture); reranking/classify models are not yet registered.
| Audio | ✅ correctness-gated | ✅ | ◐ | ◐ |
| Video+audio GENERATION (MiniMax-H3 DiT, LTX-2.5 DiT) | ◐ H3: all three modalities COHERENT on Q4_K_M (t2va, fl2va, ref2va; §8.20); the NVFP4 arm carries the patch grid; GGUF/NVFP4/bf16 loaders, pruned too (§8.21). LTX-2.5: a second lane, `SPIKE`, gated at reduced dims | ✅ H3 (vllm-omni, BF16-only, no quantized arm); LTX-2.5 only through the generic diffusers adapter, no native recipe ([vllm-omni#6066](https://github.com/vllm-project/vllm-omni/issues/6066)) | ☐ | ☐ |
| Speech / audio GENERATION (TTS, vLLM-Omni lane) | ◐ IndexTTS-2.5: vllm_synthesize renders TEXT to AUDIO on real weights, but the reference clip is IGNORED and CAMPPlus returns NaN on real weights (#634, #633) | ✅ (vllm-omni: MOSS-TTS, Qwen3-TTS, Higgs Audio v3, Voxtral TTS, IndexTTS-2.5) | not assessed | not assessed |
| MUSIC generation (MiniMax-Music3) | ✓ every stage gated, and a composed HTTP request observed end to end on CPU: a 44100 Hz stereo WAV in 7:54 (#852, [spec](../.agents/specs/minimax-music3.md), #672) | ☐ absent from the pin, from vLLM `main` and from `vllm-omni` alike | ◐ served by SGLang-Omni, a third repository, which loads the NATIVE checkpoint layout | ☐ |
| MUSIC generation (MiniMax-Music3) | ✓ every stage gated; an HTTP request observed e2e over a REAL SOCKET against a MUSIC-ONLY server (#852, #672, [spec](../.agents/specs/minimax-music3.md) §10) | ☐ absent from the pin, from vLLM `main` and from `vllm-omni` | ◐ SGLang-Omni serves the NATIVE layout; its 32 kHz resample and batching are OWED | ☐ |
| Multimodal over the OpenAI server | ◐ image request path wired, forward pending | ✅ | ✅ | ◐ |

Image, video and audio are correct through the CLI and library. Over the HTTP
Expand Down
Loading
Loading