Skip to content
Merged
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ The format is based on Keep a Changelog.

## Unreleased

### Music

- accelerated MiniMax Music 3 on Apple Silicon with a reachable 16,385-row
semantic head, fused QKV and gate/up projections, incremental residual-depth
KV caches, cached rotary/zero conditioning, batched flow guidance, and fewer
forced evaluations. The exact upstream graph remains available through
`--performance-mode reference`; optimized BF16 is the default.
- added opt-in `q8` and `q4` group-64 affine turbo modes across the language
and depth transformers. On an M4 Max, a matched 250-frame/30-step staged
render measured 143.29 s in the 0.37.0 reference runtime, 105.46 s in optimized
BF16 before the duration-floor correction, 52.66 s in Q8, and 51.40 s in Q4.
A checkpoint gate measured Q8/Q4 semantic-logit cosine 0.99985/0.99166 and
top-100 overlap 98/80 against optimized BF16. Whole-flow compilation and
affine flow quantization were measured and rejected because both regressed;
batched flow CFG beat the serial alternative by 6.0% on a matched probe. A
strict three-minute Q8 render completed in 1,413.81 s with 4.50 GB maximum
resident memory and decoded to 180.268 s.
- verified `--performance-mode reference` end to end against a clean 0.37.0
executable: matched 250-frame renders were byte-identical after PCM24 export,
including deterministic dither.
- added `--minimum-duration` and `--min-frames` plus speech-request
`minimum_audio_duration` and `min_new_tokens`. EOS remains masked through the
requested floor, and second-based floors account for whole vocoder hops so a
nominal 10-second request no longer decodes to 9.996 seconds.

## 0.37.0 - 2026-08-13

This release adds two substantial native Apple Silicon runtimes: LiquidAI's
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,15 +606,18 @@ swift run mere.run music generate \
--model music-minimax-music3 \
--lyrics-file ./lyrics.txt \
--duration 30 \
--minimum-duration 30 \
--steps 30 \
--seed 7 \
--memory-mode staged \
--performance-mode q8 \
--output ./minimax-song.wav

# Keep MiniMax Music 3 warm behind its speech-compatible HTTP route
swift run mere.run music serve \
--model music-minimax-music3 \
--memory-mode resident \
--performance-mode q8 \
--port 8080

# Generate with ACE-Step XL Turbo on larger Macs
Expand Down
59 changes: 55 additions & 4 deletions Sources/MereRunCLI/Commands/MiniMaxMusic3Serve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct MiniMaxMusic3SpeechRequest: Codable, Sendable {
var maxNewTokens: Int?
var stream: Bool?
var audioDuration: Float?
var minimumAudioDuration: Float?
var minNewTokens: Int?
var numInferenceSteps: Int?
var guidanceScale: Float?
var sampleRate: Int?
Expand All @@ -26,6 +28,8 @@ struct MiniMaxMusic3SpeechRequest: Codable, Sendable {
case maxNewTokens = "max_new_tokens"
case stream
case audioDuration = "audio_duration"
case minimumAudioDuration = "minimum_audio_duration"
case minNewTokens = "min_new_tokens"
case numInferenceSteps = "num_inference_steps"
case guidanceScale = "guidance_scale"
case sampleRate = "sample_rate"
Expand All @@ -37,6 +41,7 @@ private struct MiniMaxMusic3HealthResponse: Codable {
var model: String
var resident: Bool
var memoryMode: MiniMaxMusic3LoadingStrategy
var performanceMode: MiniMaxMusic3PerformanceMode
var nativeSampleRate: Int
var speechSampleRate: Int

Expand All @@ -45,6 +50,7 @@ private struct MiniMaxMusic3HealthResponse: Codable {
case model
case resident
case memoryMode = "memory_mode"
case performanceMode = "performance_mode"
case nativeSampleRate = "native_sample_rate"
case speechSampleRate = "speech_sample_rate"
}
Expand All @@ -55,11 +61,13 @@ private actor MiniMaxMusic3ServerSession {

init(
resources: MiniMaxMusic3Resources,
loadingStrategy: MiniMaxMusic3LoadingStrategy
loadingStrategy: MiniMaxMusic3LoadingStrategy,
performanceMode: MiniMaxMusic3PerformanceMode
) throws {
self.pipeline = try MiniMaxMusic3Pipeline(
resources: resources,
loadingStrategy: loadingStrategy
loadingStrategy: loadingStrategy,
performanceMode: performanceMode
)
}

Expand Down Expand Up @@ -119,7 +127,14 @@ private actor MiniMaxMusic3ServerSession {
throw ValidationError("audio_duration must be greater than 0 and at most 360 seconds.")
}
let durationFrames = Int(duration * Float(MiniMaxMusic3Prompt.frameRate))
let maximumFrames = request.maxNewTokens ?? durationFrames
let durationFloorFrames = request.minimumAudioDuration.map {
MiniMaxMusic3Prompt.minimumFrameCount(forDurationSeconds: $0)
}
let minimumFrames = request.minNewTokens ?? durationFloorFrames
let maximumFrames = request.maxNewTokens
?? (request.minimumAudioDuration == nil
? durationFrames
: max(durationFrames, minimumFrames ?? 0))
guard (1...MiniMaxMusic3Prompt.maxAudioFrames).contains(maximumFrames) else {
throw ValidationError("max_new_tokens must be between 1 and 9000.")
}
Expand All @@ -131,6 +146,36 @@ private actor MiniMaxMusic3ServerSession {
"audio_duration and max_new_tokens must describe the same 25 Hz frame limit."
)
}
if let minimumAudioDuration = request.minimumAudioDuration,
minimumAudioDuration <= 0 || minimumAudioDuration > 360
{
throw ValidationError(
"minimum_audio_duration must be greater than 0 and at most 360 seconds."
)
}
if request.audioDuration != nil,
let minimumAudioDuration = request.minimumAudioDuration,
minimumAudioDuration > duration
{
throw ValidationError(
"minimum_audio_duration cannot exceed audio_duration."
)
}
if let minimumFrames,
!(1...MiniMaxMusic3Prompt.maxAudioFrames).contains(minimumFrames)
{
throw ValidationError("min_new_tokens must be between 1 and 9000.")
}
if let durationFloorFrames, let requestedMinimum = request.minNewTokens,
durationFloorFrames != requestedMinimum
{
throw ValidationError(
"minimum_audio_duration and min_new_tokens must describe the same 25 Hz frame floor."
)
}
if let minimumFrames, minimumFrames > maximumFrames {
throw ValidationError("The requested duration floor cannot exceed the output upper bound.")
}
let steps = request.numInferenceSteps ?? 30
guard steps > 0 else {
throw ValidationError("num_inference_steps must be positive.")
Expand All @@ -148,6 +193,7 @@ private actor MiniMaxMusic3ServerSession {
caption: caption,
lyrics: lyrics,
durationSeconds: duration,
minimumFrames: minimumFrames,
maximumFrames: maximumFrames,
inferenceSteps: steps,
seed: request.seed ?? 0,
Expand All @@ -162,20 +208,24 @@ final class MiniMaxMusic3APIServer: @unchecked Sendable {
private let session: MiniMaxMusic3ServerSession
private let modelID: String
private let loadingStrategy: MiniMaxMusic3LoadingStrategy
private let performanceMode: MiniMaxMusic3PerformanceMode
private let apiKey: String?

init(
resources: MiniMaxMusic3Resources,
modelID: String,
loadingStrategy: MiniMaxMusic3LoadingStrategy,
performanceMode: MiniMaxMusic3PerformanceMode,
apiKey: String?
) throws {
self.session = try MiniMaxMusic3ServerSession(
resources: resources,
loadingStrategy: loadingStrategy
loadingStrategy: loadingStrategy,
performanceMode: performanceMode
)
self.modelID = modelID
self.loadingStrategy = loadingStrategy
self.performanceMode = performanceMode
self.apiKey = apiKey
}

Expand Down Expand Up @@ -203,6 +253,7 @@ final class MiniMaxMusic3APIServer: @unchecked Sendable {
model: modelID,
resident: loadingStrategy == .resident,
memoryMode: loadingStrategy,
performanceMode: performanceMode,
nativeSampleRate: 44_100,
speechSampleRate: 32_000
)
Expand Down
83 changes: 81 additions & 2 deletions Sources/MereRunCLI/Commands/MusicGenerateCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ struct MusicGenerate: AsyncParsableCommand {
@Option(name: [.customLong("duration")], help: "Output upper bound in seconds (MiniMax defaults to 60; ACE-Step song/final may plan it when omitted).")
var durationSeconds: Float?

@Option(
name: [.customLong("minimum-duration")],
help: "MiniMax Music 3 minimum output duration in seconds; EOS is masked until this floor."
)
var miniMaxMinimumDurationSeconds: Float?

@Option(
name: [.customLong("min-frames")],
help: "MiniMax Music 3 minimum 25 Hz acoustic frames (1...9000); must agree with --minimum-duration."
)
var miniMaxMinimumFrames: Int?

@Option(
name: [.customLong("max-frames")],
help: "MiniMax Music 3 exact 25 Hz acoustic-frame limit (1...9000); must agree with --duration when both are set."
Expand All @@ -174,6 +186,12 @@ struct MusicGenerate: AsyncParsableCommand {
)
var miniMaxLoadingStrategy: MiniMaxMusic3LoadingStrategy?

@Option(
name: [.customLong("performance-mode")],
help: "MiniMax Music 3 execution: reference, optimized (default), q8, or q4."
)
var miniMaxPerformanceMode: MiniMaxMusic3PerformanceMode?

@Option(name: [.customLong("quality")], help: "Adaptive ACE-Step quality: draft, song, final, or edit.")
var quality: ACEStepQualityPreset?

Expand Down Expand Up @@ -509,11 +527,14 @@ struct MusicGenerate: AsyncParsableCommand {
}

if miniMaxMaximumFrames != nil
|| miniMaxMinimumFrames != nil
|| miniMaxMinimumDurationSeconds != nil
|| miniMaxOutputSampleRate != nil
|| miniMaxLoadingStrategy != nil
|| miniMaxPerformanceMode != nil
{
throw ValidationError(
"--max-frames, --sample-rate, and --memory-mode require MiniMax Music 3."
"MiniMax duration-frame, sample-rate, memory-mode, and performance-mode options require MiniMax Music 3."
)
}

Expand Down Expand Up @@ -1159,20 +1180,30 @@ struct MusicGenerate: AsyncParsableCommand {
if !quiet {
CLIStderr.write("MiniMax memory mode: \(loadingStrategy.rawValue)\n")
}
let performanceMode = miniMaxPerformanceMode ?? .optimized
if !quiet {
CLIStderr.write("MiniMax performance mode: \(performanceMode.rawValue)\n")
}
let pipeline = try MiniMaxMusic3Pipeline(
resources: resources,
loadingStrategy: loadingStrategy
loadingStrategy: loadingStrategy,
performanceMode: performanceMode
)
let requestedDuration = explicitDurationSeconds
?? miniMaxMaximumFrames.map {
Float($0) / Float(MiniMaxMusic3Prompt.frameRate)
}
?? 60
let requestedMinimumFrames = miniMaxMinimumFrames
?? miniMaxMinimumDurationSeconds.map {
MiniMaxMusic3Prompt.minimumFrameCount(forDurationSeconds: $0)
}
let result = try pipeline.generate(
options: MiniMaxMusic3GenerationOptions(
caption: caption,
lyrics: resolvedLyrics,
durationSeconds: requestedDuration,
minimumFrames: requestedMinimumFrames,
maximumFrames: miniMaxMaximumFrames,
inferenceSteps: steps ?? 30,
seed: seed ?? 0,
Expand Down Expand Up @@ -1228,6 +1259,7 @@ struct MusicGenerate: AsyncParsableCommand {
caption: caption,
lyrics: resolvedLyrics,
durationSeconds: requestedDuration,
requestedMinimumFrames: requestedMinimumFrames,
requestedMaximumFrames: miniMaxMaximumFrames,
generatedFrameCount: result.frameCount,
inferenceSteps: steps ?? 30,
Expand All @@ -1236,6 +1268,7 @@ struct MusicGenerate: AsyncParsableCommand {
nativeSampleRate: result.sampleRate,
outputSampleRate: outputSampleRate,
loadingStrategy: loadingStrategy,
performanceMode: performanceMode,
export: exportOptions,
outputFilename: outputURL.lastPathComponent,
outputSHA256: try ModelArtifactPin.fileSHA256(outputURL)
Expand All @@ -1255,6 +1288,26 @@ struct MusicGenerate: AsyncParsableCommand {
}

func validateMiniMaxMusic3Options(explicitDurationSeconds: Float?) throws {
if let miniMaxMinimumDurationSeconds,
miniMaxMinimumDurationSeconds <= 0 || miniMaxMinimumDurationSeconds > 360
{
throw ValidationError("--minimum-duration must be greater than 0 and at most 360 seconds.")
}
if let miniMaxMinimumFrames,
!(1...MiniMaxMusic3Prompt.maxAudioFrames).contains(miniMaxMinimumFrames)
{
throw ValidationError("--min-frames must be between 1 and 9000.")
}
if let miniMaxMinimumDurationSeconds, let miniMaxMinimumFrames,
MiniMaxMusic3Prompt.minimumFrameCount(
forDurationSeconds: miniMaxMinimumDurationSeconds
)
!= miniMaxMinimumFrames
{
throw ValidationError(
"--minimum-duration and --min-frames must describe the same 25 Hz frame floor."
)
}
if let miniMaxMaximumFrames,
!(1...MiniMaxMusic3Prompt.maxAudioFrames).contains(miniMaxMaximumFrames)
{
Expand All @@ -1276,6 +1329,32 @@ struct MusicGenerate: AsyncParsableCommand {
if let explicitDurationSeconds, explicitDurationSeconds > 360 {
throw ValidationError("MiniMax Music 3 supports at most 360 seconds (9,000 frames at 25 Hz).")
}
if let explicitDurationSeconds, let miniMaxMinimumDurationSeconds,
miniMaxMinimumDurationSeconds > explicitDurationSeconds
{
throw ValidationError("MiniMax Music 3 minimum duration cannot exceed its output upper bound.")
}
let resolvedMinimumFrames = miniMaxMinimumFrames
?? miniMaxMinimumDurationSeconds.map {
MiniMaxMusic3Prompt.minimumFrameCount(forDurationSeconds: $0)
}
if let resolvedMinimumFrames,
resolvedMinimumFrames > MiniMaxMusic3Prompt.maxAudioFrames
{
throw ValidationError(
"MiniMax Music 3 minimum duration exceeds the decoded duration available from 9,000 frames."
)
}
let durationFrames = explicitDurationSeconds.map {
Int($0 * Float(MiniMaxMusic3Prompt.frameRate))
} ?? 60 * MiniMaxMusic3Prompt.frameRate
let resolvedMaximumFrames = miniMaxMaximumFrames
?? (miniMaxMinimumDurationSeconds == nil
? durationFrames
: max(durationFrames, resolvedMinimumFrames ?? 0))
if let resolvedMinimumFrames, resolvedMinimumFrames > resolvedMaximumFrames {
throw ValidationError("MiniMax Music 3 minimum duration cannot exceed its output upper bound.")
}
if useLM || noLM || analyzeSourceAudio || lmModel != nil || lmSubdirectory != nil {
throw ValidationError("MiniMax Music 3 uses its built-in autoregressive stage; ACE-Step LM options do not apply.")
}
Expand Down
15 changes: 12 additions & 3 deletions Sources/MereRunCLI/Commands/MusicServeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ struct MusicServe: AsyncParsableCommand {
)
var miniMaxLoadingStrategy: MiniMaxMusic3LoadingStrategy?

@Option(
name: [.customLong("performance-mode")],
help: "MiniMax Music 3 execution: reference, optimized (default), q8, or q4."
)
var miniMaxPerformanceMode: MiniMaxMusic3PerformanceMode?

@Option(name: [.customLong("checkpoints-root")], help: "Root containing ACE-Step checkpoint directories.")
var checkpointsRoot: String?

Expand Down Expand Up @@ -82,8 +88,8 @@ struct MusicServe: AsyncParsableCommand {
try await runMiniMaxMusic3(apiKey: resolvedAPIKey)
return
}
if miniMaxLoadingStrategy != nil {
throw ValidationError("--memory-mode requires MiniMax Music 3.")
if miniMaxLoadingStrategy != nil || miniMaxPerformanceMode != nil {
throw ValidationError("--memory-mode and --performance-mode require MiniMax Music 3.")
}

let root = try await ACEStepCLIHelper.resolveCheckpointsRoot(
Expand Down Expand Up @@ -192,13 +198,16 @@ struct MusicServe: AsyncParsableCommand {
)
}
let loadingStrategy = miniMaxLoadingStrategy ?? .resident
let performanceMode = miniMaxPerformanceMode ?? .optimized
CLIStderr.write(
"Loading MiniMax Music 3 server in \(loadingStrategy.rawValue) memory mode\n"
"Loading MiniMax Music 3 server in \(loadingStrategy.rawValue) memory mode "
+ "with \(performanceMode.rawValue) execution\n"
)
let server = try MiniMaxMusic3APIServer(
resources: resources,
modelID: ModelResolver.ModelID.miniMaxMusic3.rawValue,
loadingStrategy: loadingStrategy,
performanceMode: performanceMode,
apiKey: apiKey
)
try await server.run(host: host, port: port)
Expand Down
Loading