From b958d4999b2d5ef137d5f6b82a24ec7802e0f110 Mon Sep 17 00:00:00 2001 From: Kyle McCullough Date: Fri, 14 Aug 2026 06:23:53 -0300 Subject: [PATCH] Add MiniMax-H3 Ref2VA Turbo adapter --- CHANGELOG.md | 8 ++ README.md | 3 + .../MereRunCLI/Commands/VideoCommand.swift | 23 ++++- .../Support/VideoGenerationPreflight.swift | 94 +++++++++++++++---- .../MereRunCore/ManagedAdapterCatalog.swift | 23 +++++ .../MiniMaxH3/MiniMaxH3Generator.swift | 52 +++++++--- .../MiniMaxH3/MiniMaxH3TurboAdapter.swift | 25 +++++ Sources/MereRunCore/MiniMaxH3/README.md | 15 ++- .../MereRunCLITests/AdapterCommandTests.swift | 8 ++ Tests/MereRunCLITests/VideoCommandTests.swift | 31 ++++++ .../ManagedAdapterCatalogTests.swift | 16 ++++ Tests/MereRunCoreTests/MiniMaxH3Tests.swift | 32 +++++++ docs/cli.md | 18 ++-- docs/model-sources.md | 12 +++ docs/runtime/model-management.md | 1 + docs/runtime/video.md | 17 +++- 16 files changed, 328 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f3e5d3..88deadb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,14 @@ The format is based on Keep a Changelog. requested floor, and second-based floors account for whole vocoder hops so a nominal 10-second request no longer decodes to 9.996 seconds. +### Video + +- added the checksum-pinned LightX2V MiniMax-H3 Ref2VA Turbo 4-step v0.1 + adapter to `adapter pull`. The native Ref2VA path selects the published + four-evaluation, video/audio shift 12/3, alpha-8 recipe and fuses its 312 + PEFT pairs after the managed INT8 transformer is admitted and expanded to + resident BF16. + ## 0.37.0 - 2026-08-13 This release adds two substantial native Apple Silicon runtimes: LiquidAI's diff --git a/README.md b/README.md index b94aa315..aec19a04 100644 --- a/README.md +++ b/README.md @@ -859,12 +859,15 @@ swift run mere.run video generate \ # Managed 8-bit Ref2VA preserves reference order semantically swift run mere.run model pull video-minimax-h3-ref2va-mlx --accept-model-license +swift run mere.run adapter pull minimax-h3-lightx2v-ref2v-4step-v0.1 swift run mere.run video generate \ "keep the subject, borrow the camera move, and follow the vocal rhythm" \ --model video-minimax-h3-ref2va-mlx \ --reference image:./subject.png \ --reference video:./camera-and-soundtrack.mp4 \ --reference audio:./voice.wav \ + --h3-adapter minimax-h3-lightx2v-ref2v-4step-v0.1 \ + --h3-weight-mode resident-bf16 \ --num-frames 124 \ --output ./referenced-h3.mp4 diff --git a/Sources/MereRunCLI/Commands/VideoCommand.swift b/Sources/MereRunCLI/Commands/VideoCommand.swift index 503adf0f..fd62c474 100644 --- a/Sources/MereRunCLI/Commands/VideoCommand.swift +++ b/Sources/MereRunCLI/Commands/VideoCommand.swift @@ -1167,13 +1167,28 @@ struct VideoGenerate: AsyncParsableCommand { } let h3Resources = MiniMaxH3Resources(rootURL: resolvedRootURL) let h3Configuration = try h3Resources.loadConfiguration() - if h3Adapter != nil, !h3Resources.usesShardedBF16Transformer { - throw ValidationError("--h3-adapter currently requires the MiniMax-H3 BF16 FL2VA model.") - } + let h3AdapterBaseModelID = h3Configuration.task == MiniMaxH3TurboAdapter.Task.ref2va.rawValue + ? ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue + : ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue let resolvedH3Adapter = try ManagedAdapterArgumentResolver.resolve( h3Adapter, - baseModelID: ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue + baseModelID: h3AdapterBaseModelID ).map { URL(fileURLWithPath: $0).standardizedFileURL } + let h3AdapterRecipe = resolvedH3Adapter.map(MiniMaxH3TurboAdapter.inferenceRecipe(for:)) + if let h3AdapterRecipe, + !h3AdapterRecipe.supports(task: h3Configuration.task) { + throw ValidationError( + "MiniMax-H3 adapter \(h3AdapterRecipe.name) requires \(h3AdapterRecipe.task.rawValue), not \(h3Configuration.task)." + ) + } + if h3AdapterRecipe?.task == .fl2va, !h3Resources.usesShardedBF16Transformer { + throw ValidationError("MiniMax-H3 FL2VA adapters require the BF16 FL2VA model.") + } + if h3AdapterRecipe?.task == .ref2va, h3WeightMode == .quantized { + throw ValidationError( + "MiniMax-H3 Ref2VA Turbo requires resident BF16 weights; use --h3-weight-mode resident-bf16." + ) + } let parsedReferences = try parseMiniMaxH3References() let parsedFrameInputs = try parseMiniMaxH3FrameInputs() if h3Configuration.task == "fl2va", !parsedReferences.isEmpty { diff --git a/Sources/MereRunCLI/Support/VideoGenerationPreflight.swift b/Sources/MereRunCLI/Support/VideoGenerationPreflight.swift index f57252f2..d42cfb47 100644 --- a/Sources/MereRunCLI/Support/VideoGenerationPreflight.swift +++ b/Sources/MereRunCLI/Support/VideoGenerationPreflight.swift @@ -480,6 +480,20 @@ struct VideoGenerationPreflightAnalyzer { return resources.validate().isEmpty && (try? resources.loadConfiguration()) != nil } + private var usesMiniMaxH3Ref2VA: Bool { + let requested = input.model.trimmingCharacters(in: .whitespacesAndNewlines) + if requested == ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue { + return true + } + if requested == ModelResolver.ModelID.miniMaxH3FL2VAMLX.rawValue + || requested == ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue { + return false + } + let candidate = input.modelRoot ?? input.model + let resources = MiniMaxH3Resources(rootURL: URL(fileURLWithPath: candidate).standardizedFileURL) + return (try? resources.loadConfiguration().task) == MiniMaxH3TurboAdapter.Task.ref2va.rawValue + } + private var usesAudioConditioning: Bool { guard let audio = input.audio else { return false } return !audio.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -801,13 +815,40 @@ struct VideoGenerationPreflightAnalyzer { message: "--h3-adapter-strength must be > 0." )) } - if input.h3Adapter != nil, !input.references.isEmpty { - diagnostics.append(PreflightDiagnostic( - id: "h3_adapter_ref2va_unsupported", - severity: .blocker, - title: "MiniMax-H3 Turbo does not support Ref2VA", - message: "Use the Turbo adapter for FL2VA text or keyframe generation without --reference." - )) + if let h3AdapterInferenceRecipe { + let requestedTask: MiniMaxH3TurboAdapter.Task = usesMiniMaxH3Ref2VA ? .ref2va : .fl2va + if h3AdapterInferenceRecipe.task != requestedTask { + diagnostics.append(PreflightDiagnostic( + id: "h3_adapter_task_mismatch", + severity: .blocker, + title: "MiniMax-H3 adapter task does not match the model", + message: "Adapter \(h3AdapterInferenceRecipe.name) requires \(h3AdapterInferenceRecipe.task.rawValue), not \(requestedTask.rawValue)." + )) + } + if h3AdapterInferenceRecipe.task == .fl2va, !input.references.isEmpty { + diagnostics.append(PreflightDiagnostic( + id: "h3_fl2va_adapter_with_references", + severity: .blocker, + title: "MiniMax-H3 FL2VA adapter cannot use references", + message: "Use the FL2VA adapter for text or keyframe generation without --reference." + )) + } + if h3AdapterInferenceRecipe.task == .ref2va, input.references.isEmpty { + diagnostics.append(PreflightDiagnostic( + id: "h3_ref2va_adapter_without_references", + severity: .blocker, + title: "MiniMax-H3 Ref2VA adapter requires references", + message: "Add at least one ordered image or video --reference." + )) + } + if h3AdapterInferenceRecipe.task == .ref2va, input.h3WeightMode == "quantized" { + diagnostics.append(PreflightDiagnostic( + id: "h3_ref2va_adapter_requires_resident_bf16", + severity: .blocker, + title: "MiniMax-H3 Ref2VA Turbo requires resident BF16 weights", + message: "Use --h3-weight-mode resident-bf16 on a machine with sufficient memory." + )) + } } if input.prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { diagnostics.append( @@ -1054,22 +1095,37 @@ struct VideoGenerationPreflightAnalyzer { )) } if input.h3Adapter != nil, usesMiniMaxH3Geometry { - let usesBF16: Bool - if let path = model.path { - usesBF16 = MiniMaxH3Resources( - rootURL: URL(fileURLWithPath: path).standardizedFileURL - ).usesShardedBF16Transformer - } else { - usesBF16 = model.requested == ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue - } - if !usesBF16 { + let expectedBaseModelID = usesMiniMaxH3Ref2VA + ? ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue + : ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue + if let reference = input.h3Adapter, + let spec = ManagedAdapterCatalog.spec(for: reference), + spec.baseModelID != expectedBaseModelID { diagnostics.append(PreflightDiagnostic( - id: "h3_adapter_requires_bf16", + id: "h3_adapter_base_model_mismatch", severity: .blocker, - title: "MiniMax-H3 Turbo requires the BF16 base model", - message: "Use --model \(ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue)." + title: "MiniMax-H3 adapter base model does not match", + message: "Adapter \(spec.id) requires \(spec.baseModelID), not \(expectedBaseModelID)." )) } + if h3AdapterInferenceRecipe?.task == .fl2va { + let usesBF16: Bool + if let path = model.path { + usesBF16 = MiniMaxH3Resources( + rootURL: URL(fileURLWithPath: path).standardizedFileURL + ).usesShardedBF16Transformer + } else { + usesBF16 = model.requested == ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue + } + if !usesBF16 { + diagnostics.append(PreflightDiagnostic( + id: "h3_adapter_requires_bf16", + severity: .blocker, + title: "MiniMax-H3 FL2VA Turbo requires the BF16 base model", + message: "Use --model \(ModelResolver.ModelID.miniMaxH3FL2VABF16MLX.rawValue)." + )) + } + } if let steps = input.steps, let recipe = h3AdapterInferenceRecipe, !recipe.supports(schedulePointCount: steps) { diff --git a/Sources/MereRunCore/ManagedAdapterCatalog.swift b/Sources/MereRunCore/ManagedAdapterCatalog.swift index 3c7c9a99..f1bf66fa 100644 --- a/Sources/MereRunCore/ManagedAdapterCatalog.swift +++ b/Sources/MereRunCore/ManagedAdapterCatalog.swift @@ -89,6 +89,8 @@ public enum ManagedAdapterCatalog { public static let miniMaxH3LightX2VEightStepV1ID = "minimax-h3-lightx2v-8step-v1" public static let miniMaxH3LightX2VFourStepV1_768pID = "minimax-h3-lightx2v-4step-v1-768p" public static let miniMaxH3LightX2VV1Revision = "e6346777701aa2b64d42ed058cdd71ae00e7cd52" + public static let miniMaxH3LightX2VRef2VFourStepV01ID = "minimax-h3-lightx2v-ref2v-4step-v0.1" + public static let miniMaxH3LightX2VRef2VFourStepV01Revision = "5d1d4829fe614c1b93fcfd9cc7718e9ba71f73e1" public static let ltx25PixelSpatialUpscalerID = "ltx25-pixel-spatial-upscaler-x2" public static let ltx25PixelSpatialUpscalerRevision = "74c4e68ee7dd99f3997d5a1bb1a3784941822222" @@ -218,6 +220,27 @@ public enum ManagedAdapterCatalog { sha256: "1bdabc2e9fce20b1db563b96bcf6e46adcad4c1964f423676436bf266cc7416c" ) ), + ManagedAdapterSpec( + id: miniMaxH3LightX2VRef2VFourStepV01ID, + title: "MiniMax-H3 Ref2VA Turbo 4-step v0.1 (LightX2V)", + version: String(miniMaxH3LightX2VRef2VFourStepV01Revision.prefix(12)), + summary: "LightX2V four-evaluation PEFT LoRA for native MiniMax-H3 Ref2VA.", + baseModelID: ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue, + format: MiniMaxH3TurboAdapter.lightX2VFormat, + license: "Apache-2.0 (adapter); MiniMax-H3 Community License (base model)", + upstreamRevision: miniMaxH3LightX2VRef2VFourStepV01Revision, + releaseManifestURL: URL( + string: "https://huggingface.co/lightx2v/Minimax-h3-Turbo/commit/\(miniMaxH3LightX2VRef2VFourStepV01Revision)" + )!, + downloadURL: URL( + string: "https://huggingface.co/lightx2v/Minimax-h3-Turbo/resolve/\(miniMaxH3LightX2VRef2VFourStepV01Revision)/minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors?download=true" + )!, + artifact: ModelArtifactPin( + filename: "minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors", + byteCount: 1_383_677_768, + sha256: "9e642fc8749c74f8da5e2382877ab5c7aa37b9a73b7fd0d6d457bd1b3cb1ae99" + ) + ), ManagedAdapterSpec( id: ltx25PixelSpatialUpscalerID, title: "LTX-2.5 Pixel Spatial Upscaler x2", diff --git a/Sources/MereRunCore/MiniMaxH3/MiniMaxH3Generator.swift b/Sources/MereRunCore/MiniMaxH3/MiniMaxH3Generator.swift index 11df87d2..820b7843 100644 --- a/Sources/MereRunCore/MiniMaxH3/MiniMaxH3Generator.swift +++ b/Sources/MereRunCore/MiniMaxH3/MiniMaxH3Generator.swift @@ -663,17 +663,26 @@ public struct MiniMaxH3GenerationOptions: Sendable, Hashable { !references.contains(where: { $0.kind != .audio }) { throw MiniMaxH3GeneratorError.invalidOptions("an audio reference must be paired with an image or video") } - if adapterURL != nil { + let adapterInferenceRecipe = adapterURL.map(MiniMaxH3TurboAdapter.inferenceRecipe(for:)) + if let adapterInferenceRecipe { guard adapterStrength > 0 else { throw MiniMaxH3GeneratorError.invalidOptions("adapter strength must be greater than zero") } - guard references.isEmpty else { - throw MiniMaxH3GeneratorError.invalidOptions( - "MiniMax-H3 Turbo supports FL2VA text/keyframe generation, not Ref2VA references" - ) + switch adapterInferenceRecipe.task { + case .fl2va: + guard references.isEmpty else { + throw MiniMaxH3GeneratorError.invalidOptions( + "The selected MiniMax-H3 FL2VA adapter cannot be used with Ref2VA references" + ) + } + case .ref2va: + guard !references.isEmpty else { + throw MiniMaxH3GeneratorError.invalidOptions( + "The selected MiniMax-H3 Ref2VA adapter requires ordered references" + ) + } } } - let adapterInferenceRecipe = adapterURL.map(MiniMaxH3TurboAdapter.inferenceRecipe(for:)) let resolvedSteps: Int if let steps { resolvedSteps = steps @@ -884,12 +893,8 @@ public final class MiniMaxH3Generator: @unchecked Sendable { )) } ) - if let adapterURL { - guard resources.usesShardedBF16Transformer else { - throw MiniMaxH3GeneratorError.invalidOptions( - "MiniMax-H3 Turbo currently requires the BF16 FL2VA model" - ) - } + let adapterInferenceRecipe = adapterURL.map(MiniMaxH3TurboAdapter.inferenceRecipe(for:)) + if let adapterURL, resources.usesShardedBF16Transformer { try MiniMaxH3TurboAdapter.install( url: adapterURL, into: transformer, @@ -924,6 +929,23 @@ public final class MiniMaxH3Generator: @unchecked Sendable { )) _ = transformer.materializeResidentBF16() } + if let adapterURL, !resources.usesShardedBF16Transformer { + guard adapterInferenceRecipe?.task == .ref2va else { + throw MiniMaxH3GeneratorError.invalidOptions( + "MiniMax-H3 FL2VA adapters require the BF16 FL2VA model" + ) + } + guard transformer.usesResidentBF16 else { + throw MiniMaxH3GeneratorError.invalidOptions( + "MiniMax-H3 Ref2VA Turbo requires resident BF16 weights; use --h3-weight-mode resident-bf16 on a machine with sufficient memory" + ) + } + try MiniMaxH3TurboAdapter.install( + url: adapterURL, + into: transformer, + strength: adapterStrength + ) + } if retainsRuntime { retainedDenoisingRuntime = (cacheKey, transformer, resolvedAdaLNCache) } @@ -1563,6 +1585,12 @@ public final class MiniMaxH3Generator: @unchecked Sendable { let missing = resources.validate() guard missing.isEmpty else { throw MiniMaxH3GeneratorError.missingModelFiles(missing) } let configuration = try resources.loadConfiguration() + if let adapterInferenceRecipe = options.adapterInferenceRecipe, + !adapterInferenceRecipe.supports(task: configuration.task) { + throw MiniMaxH3GeneratorError.invalidOptions( + "MiniMax-H3 adapter recipe \(adapterInferenceRecipe.name) requires \(adapterInferenceRecipe.task.rawValue), not \(configuration.task)" + ) + } if configuration.task == "ref2va" { return try generateRef2VA( options: options, diff --git a/Sources/MereRunCore/MiniMaxH3/MiniMaxH3TurboAdapter.swift b/Sources/MereRunCore/MiniMaxH3/MiniMaxH3TurboAdapter.swift index acc8dc11..6ca8893c 100644 --- a/Sources/MereRunCore/MiniMaxH3/MiniMaxH3TurboAdapter.swift +++ b/Sources/MereRunCore/MiniMaxH3/MiniMaxH3TurboAdapter.swift @@ -9,8 +9,14 @@ public enum MiniMaxH3TurboAdapter { public static let lightX2VExpectedPairCount = 312 public static let recommendedSchedulePointCount = 5 + public enum Task: String, Sendable, Hashable { + case fl2va + case ref2va + } + public struct InferenceRecipe: Sendable, Hashable { public let name: String + public let task: Task public let defaultSchedulePointCount: Int public let supportedSchedulePointCounts: Set public let videoFlowShift: Float? @@ -20,10 +26,15 @@ public enum MiniMaxH3TurboAdapter { public func supports(schedulePointCount: Int) -> Bool { supportedSchedulePointCounts.contains(schedulePointCount) } + + public func supports(task value: String) -> Bool { + task.rawValue == value.lowercased() + } } public static let fourEvaluationRecipe = InferenceRecipe( name: "four-evaluation", + task: .fl2va, defaultSchedulePointCount: 5, supportedSchedulePointCounts: [5], videoFlowShift: nil, @@ -33,6 +44,7 @@ public enum MiniMaxH3TurboAdapter { public static let lightX2VEightStepV1Recipe = InferenceRecipe( name: "lightx2v-v1-8-step", + task: .fl2va, defaultSchedulePointCount: 9, supportedSchedulePointCounts: [5, 9], videoFlowShift: 12, @@ -42,6 +54,7 @@ public enum MiniMaxH3TurboAdapter { public static let lightX2VFourStepV1_768pRecipe = InferenceRecipe( name: "lightx2v-v1-4-step-768p", + task: .fl2va, defaultSchedulePointCount: 5, supportedSchedulePointCounts: [5], videoFlowShift: 6, @@ -49,12 +62,24 @@ public enum MiniMaxH3TurboAdapter { lightX2VAlpha: 128 ) + public static let lightX2VRef2VFourStepV01Recipe = InferenceRecipe( + name: "lightx2v-ref2v-v0.1-4-step", + task: .ref2va, + defaultSchedulePointCount: 5, + supportedSchedulePointCounts: [5], + videoFlowShift: 12, + audioFlowShift: 3, + lightX2VAlpha: 8 + ) + public static func inferenceRecipe(for url: URL) -> InferenceRecipe { inferenceRecipe(filename: url.lastPathComponent) } public static func inferenceRecipe(filename: String) -> InferenceRecipe { switch filename.lowercased() { + case "minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors": + lightX2VRef2VFourStepV01Recipe case "minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors": lightX2VEightStepV1Recipe case "minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors": diff --git a/Sources/MereRunCore/MiniMaxH3/README.md b/Sources/MereRunCore/MiniMaxH3/README.md index 836df453..4af9ebdd 100644 --- a/Sources/MereRunCore/MiniMaxH3/README.md +++ b/Sources/MereRunCore/MiniMaxH3/README.md @@ -42,7 +42,10 @@ not silently fall out of the GPU residency set. The checksum-pinned `minimax-h3-turbo-4step`, `minimax-h3-lightx2v-4step`, `minimax-h3-lightx2v-8step-v1`, and `minimax-h3-lightx2v-4step-v1-768p` LoRAs run only with the BF16 FL2VA -transformer. +transformer. `minimax-h3-lightx2v-ref2v-4step-v0.1` instead targets Ref2VA. +The Ref2VA managed package is expanded to resident BF16 before its 312 PEFT +pairs are fused, so this recipe requires `resident-bf16` or a memory-qualified +automatic selection; forced quantized execution is rejected. The EMA-850 adapter's 259 mixed-rank pairs are applied as activation-space deltas, its fused QKV rows are deinterleaved to match the runtime's global slabs, and its AdaLN deltas are included while the exact four-evaluation @@ -53,10 +56,12 @@ both an expanded converted checkpoint and per-block LoRA matmuls. Neither v1.0 recipe is treated as the legacy four-step release: the 8-step adapter defaults to nine schedule points with shifts 12/3 and also accepts its published five-point fallback, while the 768p adapter uses five points, shifts -6/3, and alpha 128. The recommended 768p canvas is 1344x768. H3 Turbo -adapters cannot be combined with Ref2VA or denoise-step cache reuse. They can -use the attention-only `balanced` and `maximum` paths; every scheduled -evaluation still executes all 50 blocks. +6/3, and alpha 128. The recommended 768p canvas is 1344x768. The Ref2VA v0.1 +recipe uses five schedule points, shifts 12/3, and alpha 8. FL2VA adapters +cannot be combined with Ref2VA references, and the Ref2VA adapter requires +them. No H3 Turbo adapter permits denoise-step cache reuse. They can use the +attention-only `balanced` and `maximum` paths; every scheduled evaluation still +executes all 50 blocks. `--h3-acceleration quality` executes every transformer block and preserves the native same-seed trajectory. At packed sequences of at least 12,000 tokens, diff --git a/Tests/MereRunCLITests/AdapterCommandTests.swift b/Tests/MereRunCLITests/AdapterCommandTests.swift index ea8236cf..87a59a41 100644 --- a/Tests/MereRunCLITests/AdapterCommandTests.swift +++ b/Tests/MereRunCLITests/AdapterCommandTests.swift @@ -42,6 +42,14 @@ struct AdapterCommandTests { #expect(command.target == ManagedAdapterCatalog.miniMaxH3LightX2VFourStepID) } + @Test("MiniMax-H3 LightX2V Ref2VA adapter pull parses its canonical id") + func parsesMiniMaxH3LightX2VRef2VAPull() throws { + let command = try AdapterPull.parse([ + ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01ID, + ]) + #expect(command.target == ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01ID) + } + @Test("Gated LTX-2.5 DFR adapter pull requires explicit terms acceptance") func parsesLTX25DFRAdapterPull() throws { let command = try AdapterPull.parse([ diff --git a/Tests/MereRunCLITests/VideoCommandTests.swift b/Tests/MereRunCLITests/VideoCommandTests.swift index 40c7869a..c0297c27 100644 --- a/Tests/MereRunCLITests/VideoCommandTests.swift +++ b/Tests/MereRunCLITests/VideoCommandTests.swift @@ -817,6 +817,37 @@ final class VideoCommandTests: XCTestCase { ) } + func testMiniMaxH3LightX2VRef2VAPreflightSelectsPublishedRecipe() throws { + let reference = FileManager.default.temporaryDirectory + .appendingPathComponent("h3-ref2va-turbo-\(UUID().uuidString).png") + XCTAssertTrue(FileManager.default.createFile(atPath: reference.path, contents: Data())) + defer { try? FileManager.default.removeItem(at: reference) } + let command = try VideoGenerate.parse([ + "preserve the reference subject in a cinematic tracking shot", + "--model", ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue, + "--reference", "image:\(reference.path)", + "--h3-adapter", ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01ID, + "--h3-weight-mode", "resident-bf16", + ]) + let envelope = command.makePreflightEnvelope( + outputURL: makeTempOutput(name: "h3-ref2va-turbo.mp4"), + fileManager: .default, + now: { Date(timeIntervalSince1970: 0) } + ) + + XCTAssertEqual(envelope.result.plan.resolvedSteps, 5) + XCTAssertEqual(envelope.result.plan.h3WeightMode, "resident-bf16") + XCTAssertFalse(envelope.diagnostics.contains { $0.id == "h3_adapter_task_mismatch" }) + XCTAssertFalse(envelope.diagnostics.contains { $0.id == "h3_adapter_base_model_mismatch" }) + XCTAssertFalse(envelope.diagnostics.contains { $0.id == "h3_ref2va_adapter_without_references" }) + XCTAssertEqual( + envelope.result.inputs.adapter?.path, + ManagedAdapterCatalog.spec( + for: ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01ID + )?.installedFileURL().path + ) + } + func testVideoGenerateSeparatesCheckpointQualityFromOutputMode() throws { let finalVideo = try VideoGenerate.parse([ "a cinematic final shot", diff --git a/Tests/MereRunCoreTests/ManagedAdapterCatalogTests.swift b/Tests/MereRunCoreTests/ManagedAdapterCatalogTests.swift index 673a61b0..06056da6 100644 --- a/Tests/MereRunCoreTests/ManagedAdapterCatalogTests.swift +++ b/Tests/MereRunCoreTests/ManagedAdapterCatalogTests.swift @@ -92,6 +92,22 @@ struct ManagedAdapterCatalogTests { #expect(fourStep.artifact.sha256 == "1bdabc2e9fce20b1db563b96bcf6e46adcad4c1964f423676436bf266cc7416c") } + @Test("MiniMax-H3 LightX2V Ref2VA release is an immutable runtime pin") + func miniMaxH3LightX2VRef2VAReleaseIsPinned() throws { + let spec = try #require( + ManagedAdapterCatalog.spec(for: ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01ID) + ) + #expect(spec.version == "5d1d4829fe61") + #expect(spec.baseModelID == ModelResolver.ModelID.miniMaxH3Ref2VAMLX.rawValue) + #expect(spec.format == MiniMaxH3TurboAdapter.lightX2VFormat) + #expect(spec.upstreamRevision == ManagedAdapterCatalog.miniMaxH3LightX2VRef2VFourStepV01Revision) + #expect(spec.artifact.filename == "minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors") + #expect(spec.artifact.byteCount == 1_383_677_768) + #expect(spec.artifact.sha256 == "9e642fc8749c74f8da5e2382877ab5c7aa37b9a73b7fd0d6d457bd1b3cb1ae99") + #expect(spec.downloadURL.host == "huggingface.co") + #expect(spec.downloadURL.absoluteString.contains(spec.upstreamRevision!)) + } + @Test("Catalog ids resolve case-insensitively") func normalizedLookup() { #expect(ManagedAdapterCatalog.spec(for: " MERE-PLATFORM-ASSISTANT ")?.version == "22") diff --git a/Tests/MereRunCoreTests/MiniMaxH3Tests.swift b/Tests/MereRunCoreTests/MiniMaxH3Tests.swift index 11079495..be8d8014 100644 --- a/Tests/MereRunCoreTests/MiniMaxH3Tests.swift +++ b/Tests/MereRunCoreTests/MiniMaxH3Tests.swift @@ -433,6 +433,38 @@ final class MiniMaxH3Tests: MereRunCoreTestCase { XCTAssertEqual(fourStep.adapterInferenceRecipe?.lightX2VAlpha, 128) } + func testLightX2VRef2VARecipeSelectsPublishedTaskStepsShiftsAndAlpha() throws { + let adapterURL = URL( + fileURLWithPath: "/tmp/minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors" + ) + let options = try MiniMaxH3GenerationOptions( + prompt: "preserve the reference subject", + width: 960, + height: 544, + numFrames: 124, + adapterURL: adapterURL, + references: [ + MiniMaxH3ReferenceInput( + kind: .image, + url: URL(fileURLWithPath: "/tmp/reference.png") + ), + ] + ) + + XCTAssertEqual(options.steps, 5) + XCTAssertEqual(options.adapterInferenceRecipe?.task, .ref2va) + XCTAssertEqual(options.adapterInferenceRecipe?.videoFlowShift, 12) + XCTAssertEqual(options.adapterInferenceRecipe?.audioFlowShift, 3) + XCTAssertEqual(options.adapterInferenceRecipe?.lightX2VAlpha, 8) + XCTAssertThrowsError(try MiniMaxH3GenerationOptions( + prompt: "missing references", + width: 960, + height: 544, + numFrames: 124, + adapterURL: adapterURL + )) + } + func testVelocityReusePolicyPreservesFirstAndFinalDenoiseEvaluations() { let policy = MiniMaxH3VelocityReusePolicy(interval: 2) diff --git a/docs/cli.md b/docs/cli.md index 70b317c7..f9dcc020 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1986,7 +1986,7 @@ AdaLN table, reference encodings, and VAEs remain loaded between windows. `--h3-adapter minimax-h3-turbo-4step` selects the separately pulled EMA-850 LoRA, while the `minimax-h3-lightx2v-*` ids select pinned LightX2V PEFT LoRAs. -All target `video-minimax-h3-fl2va-bf16-mlx`. The legacy releases use five +The FL2VA releases target `video-minimax-h3-fl2va-bf16-mlx`. The legacy releases use five schedule points (four transformer evaluations). The v1.0 8-step release defaults to nine schedule points and accepts the published five-point fallback; the v1.0 768p release uses five points, video/audio shifts 6/3, and alpha 128. @@ -1994,8 +1994,12 @@ EMA-850 runs in activation space; LightX2V is fused once into the BF16 transformer before denoising and adds no LoRA matmuls to the generation loop. Both require dense execution of all 50 blocks and prohibit denoise-step cache reuse, but -may use the attention-only `balanced` or `maximum` path. Ref2VA and the compact -quantized H3 package are rejected. The preflight report resolves the managed +may use the attention-only `balanced` or `maximum` path. The separate +`minimax-h3-lightx2v-ref2v-4step-v0.1` adapter targets +`video-minimax-h3-ref2va-mlx`, requires ordered references, and selects five +schedule points with shifts 12/3 and alpha 8. Its managed INT8 transformer is +expanded to resident BF16 before fusion, so forced quantized execution is +rejected. The preflight report resolves the managed adapter path, verifies its presence, and preserves the adapter id, strength, and resolved schedule in the declarative action. @@ -2301,6 +2305,7 @@ mere.run adapter pull minimax-h3-turbo-4step mere.run adapter pull minimax-h3-lightx2v-4step mere.run adapter pull minimax-h3-lightx2v-8step-v1 mere.run adapter pull minimax-h3-lightx2v-4step-v1-768p +mere.run adapter pull minimax-h3-lightx2v-ref2v-4step-v0.1 ``` The pull verifies the cataloged byte count and SHA-256 before atomically @@ -2309,9 +2314,10 @@ verification diagnostics go to stderr. Use the adapter id directly with `text chat --lora`, `api serve --lora`, or the matching SCAIL `video animate --distilled-adapter` option. `video animate --profile fast` selects `scail2-lightx2v-4step` and its fixed four-step schedule. -Use the MiniMax-H3 adapter ids with `video generate --h3-adapter`; their BF16 -FL2VA base remains subject to the -MiniMax-H3 Community License acceptance. +Use the MiniMax-H3 adapter ids with `video generate --h3-adapter`. FL2VA +adapters use the BF16 FL2VA base; the Ref2V adapter uses the managed Ref2VA +base expanded to resident BF16. Both base models remain subject to MiniMax-H3 +Community License acceptance. For a cross-command decision guide, see [Benchmarking](./benchmarking.md). The sections below are the command reference for each benchmark lane. diff --git a/docs/model-sources.md b/docs/model-sources.md index 26a0e12c..c07bb5e5 100644 --- a/docs/model-sources.md +++ b/docs/model-sources.md @@ -1082,6 +1082,18 @@ The runtime binds each filename to the upstream recipe so the 8-step release uses shifts 12/3 and alpha 8, while the 1344x768 four-step release uses shifts 6/3 and alpha 128. +The Ref2VA-specific `minimax-h3-lightx2v-ref2v-4step-v0.1` adapter pins the +non-ComfyUI BF16 checkpoint +`minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors` at immutable repository +revision `5d1d4829fe614c1b93fcfd9cc7718e9ba71f73e1`. The catalog verifies its +exact 1,383,677,768-byte length and SHA-256 +`9e642fc8749c74f8da5e2382877ab5c7aa37b9a73b7fd0d6d457bd1b3cb1ae99`. +The checkpoint contains 312 BF16 PEFT pairs at rank 128. mere.run applies the +published alpha 8 and video/audio shifts 12/3, expands the managed INT8 Ref2VA +transformer to resident BF16, and fuses the adapter once before its four +denoise evaluations. The adapter is Apache-2.0; the required base model remains +governed by the MiniMax-H3 Community License. + ### `video-wan22-ti2v-5b-mlx` The native Wan2.2 TI2V-5B model root is: diff --git a/docs/runtime/model-management.md b/docs/runtime/model-management.md index 4bcbee17..ca446c31 100644 --- a/docs/runtime/model-management.md +++ b/docs/runtime/model-management.md @@ -238,6 +238,7 @@ mere.run model pull text-chat-gemma4-12b-4bit mere.run adapter list mere.run adapter pull mere-platform-assistant mere.run adapter pull scail2-lightx2v-4step +mere.run adapter pull minimax-h3-lightx2v-ref2v-4step-v0.1 mere.run text chat \ --model text-chat-gemma4-12b-4bit \ --lora mere-platform-assistant \ diff --git a/docs/runtime/video.md b/docs/runtime/video.md index 90da7a97..715d0b1d 100644 --- a/docs/runtime/video.md +++ b/docs/runtime/video.md @@ -119,6 +119,7 @@ mere.run adapter pull minimax-h3-turbo-4step mere.run adapter pull minimax-h3-lightx2v-4step mere.run adapter pull minimax-h3-lightx2v-8step-v1 mere.run adapter pull minimax-h3-lightx2v-4step-v1-768p +mere.run adapter pull minimax-h3-lightx2v-ref2v-4step-v0.1 mere.run video generate "a superhero waits beneath an umbrella at a bus stop" \ --model video-minimax-h3-fl2va-bf16-mlx \ --width 960 --height 544 \ @@ -136,6 +137,8 @@ mere.run video generate "preserve the person and use the reference motion" \ --model video-minimax-h3-ref2va-mlx \ --reference image:./person.png \ --reference video:./motion.mp4 \ + --h3-adapter minimax-h3-lightx2v-ref2v-4step-v0.1 \ + --h3-weight-mode resident-bf16 \ --output ./ref2va.mp4 # Long FL2VA or Ref2VA shots keep the H3 runtime resident and condition each @@ -165,7 +168,7 @@ converting, using, or redistributing any artifact. Passing `--accept-model-license` and continuing with the download confirms that you accept those terms and agree to comply with them. -The `minimax-h3-turbo-4step` EMA-850 adapter and all three LightX2V releases +The `minimax-h3-turbo-4step` EMA-850 adapter and all four LightX2V releases are checksum-pinned separately. EMA-850 remains an activation-space adapter because its AdaLN deltas participate in schedule-cache construction. LightX2V has no AdaLN targets, so @@ -176,9 +179,15 @@ points, which are four model evaluations. LightX2V v1.0 8-step defaults to nine schedule points (eight evaluations), accepts the upstream four-evaluation fallback, and uses video/audio shifts 12/3 with alpha 8. LightX2V v1.0 768p uses five schedule points, shifts 6/3, alpha 128, and is intended for a -1344x768 canvas. They were trained for FL2VA, require the BF16 base -model, and cannot be combined with Ref2VA references or H3 denoise-step cache -reuse. Omit `--steps` to select the pinned recipe. `--h3-acceleration quality` +1344x768 canvas. Those four adapters were trained for FL2VA, require the BF16 +FL2VA base model, and cannot be combined with Ref2VA references. The separate +`minimax-h3-lightx2v-ref2v-4step-v0.1` release targets Ref2VA, uses five schedule +points with shifts 12/3 and alpha 8, and requires ordered references. mere.run +expands the managed INT8 Ref2VA transformer to resident BF16 before fusing the +adapter, so pass `--h3-weight-mode resident-bf16` unless automatic admission is +known to qualify. Forced quantized execution is rejected. No H3 adapter can use +denoise-step cache reuse. Omit `--steps` to select the pinned recipe. +`--h3-acceleration quality` keeps the fully dense path; `balanced` and `maximum` may use attention-only dynamic sparsity while still executing all 50 blocks on every model evaluation. Strength `1.0` applies an adapter's released weights exactly; the strength