From 0f688a1a1c8c0671a2783a3a2e671a7f5e4d3ba2 Mon Sep 17 00:00:00 2001 From: Joe Mattie Date: Fri, 14 Aug 2026 19:34:47 -0700 Subject: [PATCH 1/3] Guard MiniMax Music3 conv weights against BF16 storage BF16 conv kernels are unsupported by the CUDA conv path: the naive im2col lowering asserts on the kernel type and the fast path the flow transformer's 1x1 convolutions take silently corrupts, saturating the generated audio into full-scale noise. This reproduces with any package whose conv kernels are stored BF16 and with weight_type=bf16 over the published F32-conv packages. conv_safe_storage_type() falls back to F32 for the plain conv weight loads (flow preprocess/postprocess, condition encoder proj, vocoder dec_in_proj) whenever the effective storage would be BF16. The weight-norm vocoder convolutions fold at load and are unaffected. Co-Authored-By: Claude Fable 5 --- .../community_models/minimax_music3/assets.h | 10 ++++++++++ src/community_models/minimax_music3/assets.cpp | 15 +++++++++++++++ .../minimax_music3/condition_encoder.cpp | 2 +- .../minimax_music3/flow_transformer.cpp | 4 ++-- src/community_models/minimax_music3/vocoder.cpp | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/include/engine/community_models/minimax_music3/assets.h b/include/engine/community_models/minimax_music3/assets.h index dfb264ca..be44f28c 100644 --- a/include/engine/community_models/minimax_music3/assets.h +++ b/include/engine/community_models/minimax_music3/assets.h @@ -24,4 +24,14 @@ std::shared_ptr load_minimax_music3_assets( const std::filesystem::path & model_path); void validate_minimax_music3_anchors(const MiniMaxMusic3Assets & assets); +// Conv kernels must not be stored BF16: the CUDA im2col path asserts on BF16 kernel +// types in its naive lowering and silently corrupts through the fast path, which turns +// the whole flow output into clipped noise. Conv weights are a negligible share of the +// package, so BF16 requests (explicit, or Native over a BF16 source tensor) fall back +// to F32 for convolutions. +assets::TensorStorageType conv_safe_storage_type( + const assets::TensorSource & source, + const std::string & tensor_prefix, + assets::TensorStorageType requested); + } // namespace engine::models::minimax_music3 diff --git a/src/community_models/minimax_music3/assets.cpp b/src/community_models/minimax_music3/assets.cpp index e6b0c53b..2552671f 100644 --- a/src/community_models/minimax_music3/assets.cpp +++ b/src/community_models/minimax_music3/assets.cpp @@ -147,4 +147,19 @@ std::shared_ptr load_minimax_music3_assets( return assets; } +assets::TensorStorageType conv_safe_storage_type( + const assets::TensorSource & source, + const std::string & tensor_prefix, + assets::TensorStorageType requested) { + if (requested == assets::TensorStorageType::BF16) { + return assets::TensorStorageType::F32; + } + if (requested == assets::TensorStorageType::Native && + assets::ggml_type_for_tensor_dtype(source.require_metadata(tensor_prefix + ".weight").dtype) == + GGML_TYPE_BF16) { + return assets::TensorStorageType::F32; + } + return requested; +} + } // namespace engine::models::minimax_music3 diff --git a/src/community_models/minimax_music3/condition_encoder.cpp b/src/community_models/minimax_music3/condition_encoder.cpp index 10ca6444..33f21b28 100644 --- a/src/community_models/minimax_music3/condition_encoder.cpp +++ b/src/community_models/minimax_music3/condition_encoder.cpp @@ -75,7 +75,7 @@ MiniMaxMusic3ConditionWeights load_condition_weights( *out.store, source, "proj", - storage_type, + conv_safe_storage_type(source, "proj", storage_type), config.out_dim, config.condition_hidden_dim, 3, diff --git a/src/community_models/minimax_music3/flow_transformer.cpp b/src/community_models/minimax_music3/flow_transformer.cpp index 9a19b365..d37d67ff 100644 --- a/src/community_models/minimax_music3/flow_transformer.cpp +++ b/src/community_models/minimax_music3/flow_transformer.cpp @@ -56,7 +56,7 @@ MiniMaxMusic3FlowWeights load_flow_weights( *out.store, source, "preprocess_conv", - storage_type, + conv_safe_storage_type(source, "preprocess_conv", storage_type), concat_channels, concat_channels, 1, @@ -84,7 +84,7 @@ MiniMaxMusic3FlowWeights load_flow_weights( *out.store, source, "postprocess_conv", - storage_type, + conv_safe_storage_type(source, "postprocess_conv", storage_type), config.in_channels, config.in_channels, 1, diff --git a/src/community_models/minimax_music3/vocoder.cpp b/src/community_models/minimax_music3/vocoder.cpp index 88b20ee4..3f1004a3 100644 --- a/src/community_models/minimax_music3/vocoder.cpp +++ b/src/community_models/minimax_music3/vocoder.cpp @@ -86,7 +86,7 @@ Music3VocoderWeights load_vocoder_weights( *out.store, source, "dec_in_proj", - storage_type, + conv_safe_storage_type(source, "dec_in_proj", storage_type), config.decoder_input_dim, config.latent_channels / 2, 1, From c118f0fa5fb612cbcef343766325f0633b0f8950 Mon Sep 17 00:00:00 2001 From: Joe Mattie Date: Fri, 14 Aug 2026 19:34:47 -0700 Subject: [PATCH 2/3] Repair MiniMax Music3 converter and emit config sidecars The converter as shipped failed before writing any output: the variant spec builder crashed on the spec's tensor-less gguf source, and the generated spec did not pass audiocpp_gguf source matching, so every component conversion aborted. Conversion now runs with --allow-missing-model-spec until the spec sources are aligned with the runtime's filename-based component loading. Freshly converted packages also failed to load because the runtime's required config/.json sidecars were never emitted; they are now copied from the source snapshot. Conv kernels are pinned to F32 on disk through --keep-type rules for every target type, matching the runtime guard. Co-Authored-By: Claude Fable 5 --- scripts/minimax_music3/convert_gguf.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/minimax_music3/convert_gguf.py b/scripts/minimax_music3/convert_gguf.py index 9c1dc3e9..cfb14cce 100644 --- a/scripts/minimax_music3/convert_gguf.py +++ b/scripts/minimax_music3/convert_gguf.py @@ -34,6 +34,24 @@ ("vocoder", "vocoder/diffusion_pytorch_model.safetensors", "vocoder.gguf"), ] +# Conv kernels must not be stored BF16: the CUDA conv lowering corrupts on BF16 kernel +# types (and the runtime guards this at load). Keep them F32 on disk for every target +# type. +KEEP_TYPES = { + "transformer": ["preprocess_conv*=f32", "postprocess_conv*=f32"], + "condition_encoder": ["proj.weight=f32"], + "vocoder": ["dec_in_proj*=f32"], +} + +# Config sidecars the runtime requires next to the component GGUFs. +CONFIG_SIDECARS = { + "language_model": "language_model/config.json", + "rvq_depth_decoder": "rvq_depth_decoder/config.json", + "condition_encoder": "condition_encoder/config.json", + "transformer": "transformer/config.json", + "vocoder": "vocoder/config.json", +} + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) @@ -117,6 +135,7 @@ def write_variant_spec(args: argparse.Namespace) -> tempfile.TemporaryDirectory[ output_by_name = {name: output_name(args, component) for name, _input, _base in COMPONENTS for component in [next(c for c in COMPONENTS if c[0] == name)]} gguf_source = next(source for source in spec["sources"] if source["format"] == "gguf") + gguf_source.setdefault("tensors", {}) gguf_source["tensors"]["language_model_weights"] = f"model:{output_by_name['language_model']}" gguf_source["tensors"]["depth_decoder_weights"] = f"model:{output_by_name['rvq_depth_decoder']}" gguf_source["tensors"]["condition_encoder_weights"] = f"model:{output_by_name['condition_encoder']}" @@ -151,11 +170,22 @@ def run_conversion(args: argparse.Namespace, component: tuple[str, str, str]) -> "--model-spec", str(args.model_spec), "--no-sidecars", + # The runtime opens component GGUFs by filename rather than through spec tensor + # ids, and the spec's gguf source does not yet match audiocpp_gguf's + # source-matching rules, so conversion proceeds without a matching package spec. + "--allow-missing-model-spec", ] + for keep in KEEP_TYPES.get(name, []): + cmd.extend(["--keep-type", keep]) if args.overwrite: cmd.append("--overwrite") print("[convert]", name, "->", output_path, flush=True) subprocess.run(cmd, cwd=REPO_ROOT, check=True) + sidecar = CONFIG_SIDECARS.get(name) + if sidecar is not None: + config_dir = Path(args.output) / "config" + config_dir.mkdir(parents=True, exist_ok=True) + shutil.copyfile(Path(args.source) / sidecar, config_dir / f"{name}.json") def main() -> None: From 1d0ed7ab296239358301e960972be6b1699a146b Mon Sep 17 00:00:00 2001 From: Joe Mattie Date: Fri, 14 Aug 2026 19:34:47 -0700 Subject: [PATCH 3/3] Wire MiniMax Music3 into the native web UI Adds the catalog entry, advanced parameter controls, and the entry-file rule mapping the multi-component package to its language_model GGUF. The session accepts the canonical duration_seconds request option as an alias for duration_sec, which is what the UI's duration field sends. Co-Authored-By: Claude Fable 5 --- src/community_models/minimax_music3/session.cpp | 2 +- webui/configs/model_params.json | 7 +++++++ webui/configs/models_catalog.json | 1 + webui/native/dist/index.html | 16 ++++++++-------- webui/native/src/lib/catalog.ts | 6 ++++++ 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/community_models/minimax_music3/session.cpp b/src/community_models/minimax_music3/session.cpp index b543007a..b843d945 100644 --- a/src/community_models/minimax_music3/session.cpp +++ b/src/community_models/minimax_music3/session.cpp @@ -236,7 +236,7 @@ MiniMaxMusic3Request MiniMaxMusic3Session::parse_request(const runtime::TaskRequ if (out.lyrics.empty()) { throw std::runtime_error("MiniMax Music 3 requires lyrics"); } - if (const auto value = runtime::parse_finite_float_option(request.options, {"duration_sec"})) { + if (const auto value = runtime::parse_finite_float_option(request.options, {"duration_sec", "duration_seconds"})) { out.duration_sec = *value; } if (const auto value = runtime::parse_i64_option(request.options, {"num_inference_steps"})) { diff --git a/webui/configs/model_params.json b/webui/configs/model_params.json index b885294b..7f3cf2a9 100644 --- a/webui/configs/model_params.json +++ b/webui/configs/model_params.json @@ -123,6 +123,13 @@ {"name": "timesignature", "type": "text", "label": "【曲谱】timesignature", "default": "", "placeholder": "如 4"} ], + "minimax_music3": [ + {"name": "num_inference_steps", "type": "number", "label": "Flow steps per window", "default": 30, "minimum": 1, "maximum": 200, "step": 1, "precision": 0, "info": "Flow-matching Euler steps per 200-frame denoising window."}, + {"name": "guidance_scale", "type": "slider", "label": "Flow guidance scale", "default": 1.7, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "ar_guidance_scale", "type": "slider", "label": "AR guidance scale", "default": 1.5, "minimum": 0.0, "maximum": 10.0, "step": 0.1, "info": "Classifier-free guidance of the semantic and residual code sampling."}, + {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 1, "maximum": 1024, "step": 1, "precision": 0} + ], + "minimax_h3": [ {"name": "num_inference_steps", "type": "number", "label": "Denoising steps", "default": 12, "minimum": 1, "maximum": 50, "step": 1, "precision": 0, "info": "Twelve denoising steps provide a practical quality and performance balance."}, {"name": "num_frames", "type": "number", "label": "Output frames", "default": 241, "minimum": 5, "maximum": 1441, "step": 4, "precision": 0, "info": "Approximately 24 frames per output second; 241 frames produces about 10 seconds of audio."}, diff --git a/webui/configs/models_catalog.json b/webui/configs/models_catalog.json index 05cefa2e..84bbba8a 100644 --- a/webui/configs/models_catalog.json +++ b/webui/configs/models_catalog.json @@ -56,6 +56,7 @@ { "id": "chatterbox", "display_name": "Chatterbox (voice clone)", "family": "chatterbox", "path": "models/chatterbox", "task": "clon", "mode": "offline", "download_id": "chatterbox", "min_vram_gb": 12 }, { "id": "ace-step", "display_name": "ACE-Step 1.5 (music gen)", "family": "ace_step", "path": "models/Ace-Step1.5", "task": "gen", "mode": "offline", "download_id": "ace_step", "session_options": { "ace_step.mem_saver": "true", "ace_step.dit_weight_type": "q8_0", "ace_step.text_encoder_weight_type": "q8_0", "ace_step.planner_weight_type": "q8_0" }, "min_vram_gb": 8 }, + { "id": "minimax-music3", "display_name": "MiniMax-Music3 (song gen)", "family": "minimax_music3", "path": "models/MiniMax-Music3-GGUF/language_model_q4_k.gguf", "task": "gen", "mode": "offline", "download_id": "minimax_music3_q4_k", "min_vram_gb": 12 }, { "id": "stable-audio-small-music","display_name": "Stable Audio 3 Small Music (gen)", "family": "stable_audio", "path": "models/stable-audio-3-small-music", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_music", "min_vram_gb": 4 }, { "id": "stable-audio-small-sfx", "display_name": "Stable Audio 3 Small SFX (gen)", "family": "stable_audio", "path": "models/stable-audio-3-small-sfx", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_sfx", "min_vram_gb": 4 }, { "id": "stable-audio-medium", "display_name": "Stable Audio 3 Medium (gen)", "family": "stable_audio", "path": "models/stable-audio-3-medium", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_medium", "session_options": { "stable_audio.mem_saver": "true" }, "min_vram_gb": 10 }, diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index 679c6a4c..185cec48 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -13,20 +13,20 @@
diff --git a/webui/native/src/lib/catalog.ts b/webui/native/src/lib/catalog.ts index ca03e7b5..393dd795 100644 --- a/webui/native/src/lib/catalog.ts +++ b/webui/native/src/lib/catalog.ts @@ -118,6 +118,12 @@ function packageModelPath(entry: PackageEntry): string { if (entry.format === 'gguf' && entry.family === 'minimax_h3') { const entryName = entry.id.includes('int8_dit') ? 'dit_int8.gguf' : 'dit.gguf'; modelFile = entry.files?.find((file) => file.toLowerCase().endsWith(`/${entryName}`)); + } else if (entry.format === 'gguf' && entry.family === 'minimax_music3') { + // Multi-component package; the language_model_*.gguf matching the package + // precision is the entry file. + modelFile = entry.files?.find((file) => + new RegExp(`/language_model_${entry.precision}\\.gguf$`).test(file.toLowerCase())) || + entry.files?.find((file) => /\/language_model_[^/]*\.gguf$/.test(file.toLowerCase())); } else if (entry.format === 'gguf') { modelFile = entry.files?.find((file) => file.toLowerCase().endsWith('.gguf')); }