Skip to content
Open
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
6 changes: 5 additions & 1 deletion include/trtmc/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,13 @@ struct ImageResult {
};

struct AudioResult {
std::vector<float> samples; // mono float32 [-1,1]
// Interleaved float32 [-1,1]. `channels` frames share each time step, so
// samples.size() == num_samples * channels. It defaults to 1, which is what
// every speech model produces; music models set it to 2.
std::vector<float> samples;
int32_t num_samples{0};
int32_t sample_rate{24000};
int32_t channels{1};
};

struct TranscriptionStreamConfig {
Expand Down
13 changes: 10 additions & 3 deletions include/trtmc/trtmc_io.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

namespace trtmc::io {

// Write a WAV file from AudioResult (IEEE float32 mono).
// Write a WAV file from AudioResult (IEEE float32, audio.channels interleaved).
inline void write_wav(const AudioResult& audio, const std::string& path)
{
if (audio.samples.empty())
Expand All @@ -38,11 +38,16 @@ inline void write_wav(const AudioResult& audio, const std::string& path)

const int32_t num_samples = static_cast<int32_t>(audio.samples.size());
const int32_t sample_rate = audio.sample_rate;
const int16_t num_channels = 1;
const int16_t num_channels = static_cast<int16_t>(audio.channels > 0 ? audio.channels : 1);
if (audio.samples.size() % static_cast<std::size_t>(num_channels) != 0)
throw std::runtime_error("write_wav: sample count is not a multiple of the channel count");
const int16_t bits_per_sample = 32;
const int32_t byte_rate = sample_rate * num_channels * (bits_per_sample / 8);
const int16_t block_align = static_cast<int16_t>(num_channels * (bits_per_sample / 8));
const int32_t data_size = num_samples * block_align;
// num_samples counts interleaved floats, not frames, so the payload is one
// sample width per element -- multiplying by block_align would count the
// channels twice and declare a data chunk larger than the file.
const int32_t data_size = num_samples * (bits_per_sample / 8);
const int32_t chunk_size = 36 + data_size;

// RIFF header
Expand Down Expand Up @@ -129,6 +134,8 @@ inline AudioResult read_wav(const std::string& path)
// Decode samples based on format
AudioResult result;
result.sample_rate = sample_rate;
// Multi-channel input is downmixed below, so the result is always mono.
result.channels = 1;
const auto nc = std::max<int16_t>(num_channels, 1);
if (audio_format == 3 && bits_per_sample == 32)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

id = "minimax_music3"
plugin = "minimax_music3"
module = "plugin"

# MiniMax-Music3 publishes a root `config.json` carrying `model_type` and
# `architectures` but no `_class_name`, and it ships no `model_index.json`.
# `_resolve_diffusion_entrypoint` therefore declines it, so this family is
# reached through the ordinary `model_type` aliases rather than through
# `diffusion_pipeline_classes` the way `minimax_h3` is. The pipeline components
# are read from `modular_model_index.json` by the plugin instead.
aliases = [
"minimax_music3",
"minimax-music3",
"minimaxmusic3",
]
prefixes = [
"minimax_music3",
]

# Only the seven components that `modular_model_index.json` declares. The
# repository also ships `qwen_7B/` (18.48 GB) and the root `flowmatching_vae.pth`
# and `dav.pth` (10.32 GB together), none of which that index references and
# each of whose diffusers-side counterparts is present as safetensors. Skipping
# them takes the download from 57.4 GB to 28.5 GB. If a build ever reports a
# missing module, this exclusion is the first assumption to re-test.
hf_allow_patterns = [
"config.json",
"modular_model_index.json",
"LICENSE",
"condition_encoder/**",
"language_model/**",
"rvq_depth_decoder/**",
"scheduler/**",
"tokenizer/**",
"transformer/**",
"vocoder/**",
]
# Each entry is "<repository>|<path>": the checker resolves the file against
# that repository, not against whichever snapshot happens to be at hand.
hf_required_files = [
"MiniMaxAI/MiniMax-Music3|config.json",
"MiniMaxAI/MiniMax-Music3|modular_model_index.json",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""MiniMax-Music3 text-to-music family."""

from .plugin import plugin

__all__ = ["plugin"]
192 changes: 192 additions & 0 deletions python/tensorrt_model_connect/families/minimax_music3/checkpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Tensor inventory for the MiniMax-Music3 components.

Read from the published safetensors headers at revision
``fbdf52fbaaca799592917417eb05f1899f1255ec``. Each entry names the tensors a
component must carry, so a checkpoint missing a module is rejected before any
engine is built rather than producing an engine with silently absent weights.

Two shapes here contradict a plain reading of the component configs, and both
change what a builder has to do:

**Stereo comes from a mono decoder.** ``vocoder`` declares
``latent_channels = 128`` but its first layer, ``dec_in_proj``, takes 64
channels, and its last, ``conv_out``, emits 1. The 128 latent channels are two
64-channel streams decoded through the same weights, one per output channel.

**The depth decoder predicts seven codebooks, not eight.** ``rvq_depth_decoder``
declares ``num_codebooks = 8`` and carries ``audio_heads.0`` through
``audio_heads.6``, with ``audio_embeddings`` sized ``7 * 1024 = 7168``. The
first codebook comes from the global language model; the depth decoder predicts
the remaining seven.
"""

from __future__ import annotations

import re
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field

#: Number of residual codebooks the depth decoder itself predicts. The
#: component config's ``num_codebooks`` counts the language model's first
#: codebook as well.
DEPTH_DECODER_HEADS = 7

#: The vocoder decodes one audio channel at a time; the latent carries two.
VOCODER_OUTPUT_CHANNELS = 1
VOCODER_CHANNELS_PER_STREAM = 64


class CheckpointError(ValueError):
"""Raised when a component's tensors do not match the published layout."""


@dataclass(frozen=True)
class ComponentTensors:
"""Tensors one component must carry."""

name: str
#: Exact tensor names, present exactly once.
exact: tuple[str, ...] = ()
#: ``(regex, expected_count)`` for the repeated per-layer tensors.
repeated: tuple[tuple[str, int], ...] = ()
#: Tensor name to expected shape, for the shapes a builder depends on.
shapes: Mapping[str, tuple[int, ...]] = field(default_factory=dict)


CONDITION_ENCODER = ComponentTensors(
name="condition_encoder",
exact=("layer_scale", "layer_weight_logits", "proj.bias", "proj.weight"),
shapes={
# A learned weighting over the eight per-frame hidden streams -- one
# from the global language model and seven from the depth decoder,
# concatenated in `encoders.py` as `cat((last_hidden, depth_hidden))`
# -- then a Conv1d from the language model's width to the
# transformer's condition width. The component config calls these
# `num_condition_layers`, which names codebook streams, not layers.
"layer_weight_logits": (8,),
"proj.weight": (2048, 4096, 3),
"layer_scale": (1,),
},
)

RVQ_DEPTH_DECODER = ComponentTensors(
name="rvq_depth_decoder",
exact=(
"audio_embeddings.weight",
"norm.weight",
"pos_embedding.weight",
"projection.weight",
),
repeated=(
(r"^audio_heads\.\d+\.weight$", DEPTH_DECODER_HEADS),
(r"^layers\.\d+\.attn\.to_[qkv]\.weight$", 12),
(r"^layers\.\d+\.attn\.to_out\.weight$", 4),
(r"^layers\.\d+\.(gate|up)_proj\.weight$", 8),
(r"^layers\.\d+\.down_proj\.weight$", 4),
(r"^layers\.\d+\.(input|post_attention)_layernorm\.weight$", 8),
),
shapes={
"audio_embeddings.weight": (DEPTH_DECODER_HEADS * 1024, 4096),
"audio_heads.0.weight": (1024, 4096),
"pos_embedding.weight": (16, 4096),
"projection.weight": (4096, 4096),
},
)

VOCODER = ComponentTensors(
name="vocoder",
exact=(
"conv_in.bias",
"conv_in.weight_g",
"conv_in.weight_v",
"conv_out.bias",
"conv_out.weight_g",
"conv_out.weight_v",
"dec_in_proj.bias",
"dec_in_proj.weight",
"snake_out.alpha",
),
repeated=(
(r"^blocks\.\d+\.conv_t1\.(bias|weight_g|weight_v)$", 12),
(r"^blocks\.\d+\.snake1\.alpha$", 4),
(r"^blocks\.\d+\.res_unit[123]\.conv[12]\.(bias|weight_g|weight_v)$", 72),
(r"^blocks\.\d+\.res_unit[123]\.snake[12]\.alpha$", 24),
),
shapes={
# One stream in, one audio channel out.
"dec_in_proj.weight": (1024, VOCODER_CHANNELS_PER_STREAM, 1),
"conv_in.weight_v": (1536, 1024, 7),
"conv_out.weight_v": (VOCODER_OUTPUT_CHANNELS, 96, 7),
},
)

TRANSFORMER = ComponentTensors(
name="transformer",
exact=(
"preprocess_conv.weight",
"proj_in.weight",
"time_proj.weight",
"time_embed.linear_1.weight",
"time_embed.linear_1.bias",
"time_embed.linear_2.weight",
"time_embed.linear_2.bias",
),
shapes={
"proj_in.weight": (2048, 2304),
"preprocess_conv.weight": (2304, 2304, 1),
# fourier_embedding_dim 256 in, transformer width out.
"time_embed.linear_1.weight": (2048, 256),
"time_embed.linear_2.weight": (2048, 2048),
},
)

COMPONENTS: tuple[ComponentTensors, ...] = (
CONDITION_ENCODER,
RVQ_DEPTH_DECODER,
VOCODER,
TRANSFORMER,
)

_BY_NAME = {component.name: component for component in COMPONENTS}


def component(name: str) -> ComponentTensors:
"""Return the expected tensor layout of one component."""

try:
return _BY_NAME[name]
except KeyError:
raise CheckpointError(f"unknown MiniMax-Music3 component {name!r}") from None


def validate_component(name: str, tensors: Mapping[str, Iterable[int]]) -> None:
"""Check one component's tensor names and depended-on shapes.

``tensors`` maps tensor name to shape, as read from a safetensors header.
"""

spec = component(name)
present = set(tensors)

missing = [tensor for tensor in spec.exact if tensor not in present]
if missing:
raise CheckpointError(
f"{name} is missing {len(missing)} tensor(s): {', '.join(sorted(missing))}"
)

for pattern, expected in spec.repeated:
matched = sum(1 for tensor in present if re.match(pattern, tensor))
if matched != expected:
raise CheckpointError(
f"{name} has {matched} tensors matching {pattern!r}, expected {expected}"
)

for tensor, expected_shape in spec.shapes.items():
actual = tuple(int(dim) for dim in tensors[tensor])
if actual != expected_shape:
raise CheckpointError(
f"{name}.{tensor} has shape {actual}, expected {expected_shape}"
)
Loading
Loading