From 5472c67f296971438ed0d177697719d04ceaf4f7 Mon Sep 17 00:00:00 2001 From: Komorebi623 Date: Fri, 7 Aug 2026 15:32:01 +0800 Subject: [PATCH 01/25] Align MiniMax H3 conditioning with sd.cpp --- docs/minimax-h3.md | 3 + examples/cli/main.cpp | 8 +- examples/cli/sample_main.cpp | 8 +- .../components/autoencoders/vae.hpp | 3 +- .../components/text_encoders/llm.hpp | 244 +++++++++++++++++- .../pipelines/minimax_h3_pipeline.cpp | 82 ++++-- 6 files changed, 315 insertions(+), 33 deletions(-) diff --git a/docs/minimax-h3.md b/docs/minimax-h3.md index 90c51fc..f793ab8 100644 --- a/docs/minimax-h3.md +++ b/docs/minimax-h3.md @@ -61,6 +61,9 @@ MiniMax-H3 is a 24 fps audio-video model. The usual full-resolution setting is: The frame count must satisfy `17k + 5` (for example `5`, `22`, `39`, `56`). The examples below use 56 frames and 20 steps. +`ed-cli` writes MiniMax-H3 output at 24 fps even if another `--fps` value is +provided, matching sd.cpp and preserving the model's audio-video timing. + Use `--video-format mp4` for H.264/AAC MP4 output when `ffmpeg` is available. Without `--audio-vae`, the model can still generate video, but no generated audio is decoded or muxed. diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 726e737..af23a8a 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -808,6 +808,12 @@ int main(int argc, char** argv) { } if (args.video) { + int output_fps = args.fps; + const char* pipeline_name = ed_context_pipeline_name(ctx); + if (pipeline_name != nullptr && std::strcmp(pipeline_name, "minimax-h3") == 0 && output_fps != 24) { + std::fprintf(stderr, "MiniMax-H3 uses 24 fps; overriding requested fps %d\n", output_fps); + output_fps = 24; + } ed_video_generation_params_t gen_params; ed_video_generation_params_init(&gen_params); ed_image_t init_image = {}; @@ -935,7 +941,7 @@ int main(int argc, char** argv) { const std::string output_path = video_output_path(args.output_path, args.video_format); auto ed_wall_save0 = ed_wall_clock::now(); bool audio_muxed = false; - bool ed_save_ok = save_video_with_audio(output_path.c_str(), output, args.fps, &audio_muxed); + bool ed_save_ok = save_video_with_audio(output_path.c_str(), output, output_fps, &audio_muxed); ed_wall_save = ed_wall_ms(ed_wall_save0, ed_wall_clock::now()); if (!ed_save_ok) { std::fprintf(stderr, "failed to save output video: %s\n", output_path.c_str()); diff --git a/examples/cli/sample_main.cpp b/examples/cli/sample_main.cpp index afd772f..198110e 100644 --- a/examples/cli/sample_main.cpp +++ b/examples/cli/sample_main.cpp @@ -578,6 +578,12 @@ int main(int argc, char** argv) { } const double load_seconds = std::chrono::duration(load_t1 - load_t0).count(); const bool is_root = ed_context_parallel_is_root(ctx); + int output_fps = args.fps; + const char* pipeline_name = ed_context_pipeline_name(ctx); + if (args.video && pipeline_name != nullptr && std::strcmp(pipeline_name, "minimax-h3") == 0 && output_fps != 24) { + std::fprintf(stderr, "MiniMax-H3 uses 24 fps; overriding requested fps %d\n", output_fps); + output_fps = 24; + } if (is_root) { std::printf("=== ed-sample ===\n"); @@ -723,7 +729,7 @@ int main(int argc, char** argv) { } std::snprintf(fname, sizeof(fname), "vid_%06d.avi", idx); save_ok = save_mjpg_avi((out_path / fname).string().c_str(), - video_output, args.fps, 95); + video_output, output_fps, 95); } else { if (output.count <= 0 || output.images == nullptr) { std::fprintf(stderr, "empty output for prompt %d\n", idx); diff --git a/src/dit_models/components/autoencoders/vae.hpp b/src/dit_models/components/autoencoders/vae.hpp index bc109e7..711a76e 100644 --- a/src/dit_models/components/autoencoders/vae.hpp +++ b/src/dit_models/components/autoencoders/vae.hpp @@ -126,7 +126,8 @@ struct VAE : public GGMLRunner { int64_t H = input.shape()[1] / scale_factor; float tile_overlap; int tile_size_x, tile_size_y; - get_tile_sizes(tile_size_x, tile_size_y, tile_overlap, tiling_params, W, H, 1.30539f); + const float encode_tile_factor = version == VERSION_MINIMAX_H3 ? 1.0f : 1.30539f; + get_tile_sizes(tile_size_x, tile_size_y, tile_overlap, tiling_params, W, H, encode_tile_factor); LOG_DEBUG("VAE Tile size: %dx%d", tile_size_x, tile_size_y); output = tiled_compute(input, n_threads, diff --git a/src/dit_models/components/text_encoders/llm.hpp b/src/dit_models/components/text_encoders/llm.hpp index b110f3a..28c35c9 100644 --- a/src/dit_models/components/text_encoders/llm.hpp +++ b/src/dit_models/components/text_encoders/llm.hpp @@ -260,6 +260,7 @@ namespace LLM { int patch_size = 14; int spatial_merge_size = 2; int window_size = 112; + int num_position_embeddings = 0; std::vector deepstack_visual_indexes; std::set fullatt_block_indexes = {7, 15, 23, 31}; }; @@ -998,6 +999,7 @@ namespace LLM { LLMVisionArch arch_; int num_layers; int spatial_merge_size; + int num_grid_per_side; std::set fullatt_block_indexes; std::vector deepstack_visual_indexes; @@ -1006,9 +1008,13 @@ namespace LLM { const LLMVisionParams& vision_params, float eps = 1e-6f) : arch_(vision_params.arch), num_layers(vision_params.num_layers), spatial_merge_size(vision_params.spatial_merge_size), + num_grid_per_side(vision_params.num_position_embeddings > 0 ? static_cast(std::sqrt(vision_params.num_position_embeddings)) : 0), fullatt_block_indexes(vision_params.fullatt_block_indexes), deepstack_visual_indexes(vision_params.deepstack_visual_indexes) { blocks["patch_embed"] = std::shared_ptr(new VisionPatchEmbed(llama_cpp_style, arch_, vision_params.patch_size, vision_params.temporal_patch_size, vision_params.in_channels, vision_params.hidden_size)); + if (vision_params.num_position_embeddings > 0) { + blocks["pos_embed"] = std::make_shared(vision_params.num_position_embeddings, vision_params.hidden_size); + } for (int i = 0; i < num_layers; i++) { blocks["blocks." + std::to_string(i)] = std::shared_ptr(new VisionBlock(llama_cpp_style, arch_, vision_params.hidden_size, @@ -1022,6 +1028,22 @@ namespace LLM { } } + std::shared_ptr pos_embedder() { + auto it = blocks.find("pos_embed"); + if (it == blocks.end()) { + return nullptr; + } + return std::dynamic_pointer_cast(it->second); + } + + int get_num_grid_per_side() const { + return num_grid_per_side; + } + + int get_spatial_merge_size() const { + return spatial_merge_size; + } + std::vector forward_outputs(GGMLRunnerContext* ctx, ggml_tensor* pixel_values, ggml_tensor* pe, @@ -1029,6 +1051,7 @@ namespace LLM { ggml_tensor* window_inverse_index, ggml_tensor* window_mask, const std::vector* cu_window_seqlens = nullptr, + ggml_tensor* pos_embeds = nullptr, const std::string& debug_target = "") { // pixel_values: [grid_t*(H/mh/ph)*(W/mw/pw)*mh*mw, C*pt*ph*pw] // window_index: [grid_t*(H/mh/ph)*(W/mw/pw)] @@ -1039,6 +1062,9 @@ namespace LLM { auto x = patch_embed->forward(ctx, pixel_values); sd::ggml_graph_cut::mark_graph_cut(x, "llm.vision.prelude", "x"); + if (pos_embeds != nullptr) { + x = ggml_add(ctx->ggml_ctx, x, pos_embeds); + } if (debug_target == "patch_embed") { return {x}; } @@ -1109,8 +1135,9 @@ namespace LLM { ggml_tensor* window_inverse_index, ggml_tensor* window_mask, const std::vector* cu_window_seqlens = nullptr, + ggml_tensor* pos_embeds = nullptr, const std::string& debug_target = "") { - return forward_outputs(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask, cu_window_seqlens, debug_target)[0]; + return forward_outputs(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask, cu_window_seqlens, pos_embeds, debug_target)[0]; } }; @@ -1558,6 +1585,11 @@ namespace LLM { return x; } + std::shared_ptr vision_model() { + GGML_ASSERT(enable_vision); + return std::dynamic_pointer_cast(blocks["visual"]); + } + ggml_tensor* vision_forward(GGMLRunnerContext* ctx, ggml_tensor* pixel_values, ggml_tensor* pe, @@ -1565,10 +1597,10 @@ namespace LLM { ggml_tensor* window_inverse_index, ggml_tensor* window_mask, const std::vector* cu_window_seqlens = nullptr, + ggml_tensor* pos_embeds = nullptr, const std::string& debug_target = "") { GGML_ASSERT(enable_vision); - auto vision_model = std::dynamic_pointer_cast(blocks["visual"]); - return vision_model->forward(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask, cu_window_seqlens, debug_target); + return vision_model()->forward(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask, cu_window_seqlens, pos_embeds, debug_target); } std::vector vision_forward_outputs(GGMLRunnerContext* ctx, @@ -1576,10 +1608,10 @@ namespace LLM { ggml_tensor* pe, ggml_tensor* window_index, ggml_tensor* window_inverse_index, - ggml_tensor* window_mask) { + ggml_tensor* window_mask, + ggml_tensor* pos_embeds = nullptr) { GGML_ASSERT(enable_vision); - auto vision_model = std::dynamic_pointer_cast(blocks["visual"]); - return vision_model->forward_outputs(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask); + return vision_model()->forward_outputs(ctx, pixel_values, pe, window_index, window_inverse_index, window_mask, nullptr, pos_embeds); } }; @@ -1595,6 +1627,8 @@ namespace LLM { std::vector window_inverse_index_vec; std::vector cu_window_seqlens_vec; std::vector pe_vec; + std::array, 4> pos_embed_idx_data; + std::array, 4> pos_embed_weight_data; LLMRunner(LLMArch arch, ggml_backend_t backend, @@ -1641,6 +1675,10 @@ namespace LLM { if (contains(tensor_name, "visual.patch_embed.proj.bias")) { params.vision.hidden_size = pair.second.ne[0]; } + if (contains(tensor_name, "visual.pos_embed.weight")) { + params.vision.hidden_size = pair.second.ne[0]; + params.vision.num_position_embeddings = static_cast(pair.second.ne[1]); + } if (contains(tensor_name, "visual.blocks.")) { auto items = split_string(tensor_name.substr(pos), '.'); if (items.size() > 2) { @@ -1743,7 +1781,7 @@ namespace LLM { ggml_tensor* window_inverse_index, ggml_tensor* window_mask, const std::string& debug_target = "") { - auto hidden_states = model.vision_forward(ctx, pixel_values, input_pos, window_index, window_inverse_index, window_mask, &cu_window_seqlens_vec, debug_target); + auto hidden_states = model.vision_forward(ctx, pixel_values, input_pos, window_index, window_inverse_index, window_mask, &cu_window_seqlens_vec, nullptr, debug_target); return hidden_states; } @@ -2056,6 +2094,128 @@ namespace LLM { return patches; } + sd::Tensor process_video_block_patches_host(const sd::Tensor& frames) { + if (frames.empty() || frames.dim() != 5 || + frames.shape()[2] != params.vision.temporal_patch_size || + frames.shape()[3] != params.vision.in_channels || + frames.shape()[4] != 1) { + return {}; + } + + const int64_t width = frames.shape()[0]; + const int64_t height = frames.shape()[1]; + const int64_t temporal = frames.shape()[2]; + const int64_t channels = frames.shape()[3]; + const int64_t patch = params.vision.patch_size; + const int64_t merge = params.vision.spatial_merge_size; + const int64_t grid_w = width / patch; + const int64_t grid_h = height / patch; + const int64_t feature = channels * temporal * patch * patch; + const int64_t token_count = grid_h * grid_w; + if (width <= 0 || height <= 0 || grid_w <= 0 || grid_h <= 0 || + grid_w % merge != 0 || grid_h % merge != 0) { + return {}; + } + + sd::Tensor output({feature, token_count}); + int64_t token = 0; + for (int64_t block_h = 0; block_h < grid_h / merge; ++block_h) { + for (int64_t block_w = 0; block_w < grid_w / merge; ++block_w) { + for (int64_t inner_h = 0; inner_h < merge; ++inner_h) { + for (int64_t inner_w = 0; inner_w < merge; ++inner_w) { + int64_t patch_h = block_h * merge + inner_h; + int64_t patch_w = block_w * merge + inner_w; + int64_t offset = 0; + for (int64_t c = 0; c < channels; ++c) { + for (int64_t t = 0; t < temporal; ++t) { + for (int64_t y = 0; y < patch; ++y) { + for (int64_t x = 0; x < patch; ++x) { + output.index(offset++, token) = frames.index(patch_w * patch + x, + patch_h * patch + y, + t, + c, + 0); + } + } + } + } + ++token; + } + } + } + } + return output; + } + + ggml_tensor* build_qwen3_vl_patch_pos_embeds(GGMLRunnerContext* runner_ctx, + std::shared_ptr vision, + int grid_h, + int grid_w) { + auto pos_embed = vision->pos_embedder(); + GGML_ASSERT(pos_embed != nullptr); + const int num_grid_per_side = vision->get_num_grid_per_side(); + const int merge_size = vision->get_spatial_merge_size(); + GGML_ASSERT(num_grid_per_side > 0); + GGML_ASSERT(grid_h % merge_size == 0); + GGML_ASSERT(grid_w % merge_size == 0); + + for (int index = 0; index < 4; ++index) { + pos_embed_idx_data[index].clear(); + pos_embed_weight_data[index].clear(); + pos_embed_idx_data[index].reserve(static_cast(grid_h * grid_w)); + pos_embed_weight_data[index].reserve(static_cast(grid_h * grid_w)); + } + + const double max_index = static_cast(num_grid_per_side - 1); + for (int block_h = 0; block_h < grid_h / merge_size; ++block_h) { + for (int block_w = 0; block_w < grid_w / merge_size; ++block_w) { + for (int inner_h = 0; inner_h < merge_size; ++inner_h) { + const int h = block_h * merge_size + inner_h; + const double h_pos = grid_h == 1 ? 0.0 : max_index * h / static_cast(grid_h - 1); + const int h_floor = static_cast(std::floor(h_pos)); + const int h_ceil = std::min(h_floor + 1, num_grid_per_side - 1); + const double dh = h_pos - h_floor; + for (int inner_w = 0; inner_w < merge_size; ++inner_w) { + const int w = block_w * merge_size + inner_w; + const double w_pos = grid_w == 1 ? 0.0 : max_index * w / static_cast(grid_w - 1); + const int w_floor = static_cast(std::floor(w_pos)); + const int w_ceil = std::min(w_floor + 1, num_grid_per_side - 1); + const double dw = w_pos - w_floor; + const int ids[4] = { + h_floor * num_grid_per_side + w_floor, + h_floor * num_grid_per_side + w_ceil, + h_ceil * num_grid_per_side + w_floor, + h_ceil * num_grid_per_side + w_ceil, + }; + const float weights[4] = { + static_cast((1.0 - dh) * (1.0 - dw)), + static_cast((1.0 - dh) * dw), + static_cast(dh * (1.0 - dw)), + static_cast(dh * dw), + }; + for (int index = 0; index < 4; ++index) { + pos_embed_idx_data[index].push_back(ids[index]); + pos_embed_weight_data[index].push_back(weights[index]); + } + } + } + } + } + + ggml_tensor* result = nullptr; + for (int index = 0; index < 4; ++index) { + auto idx_tensor = ggml_new_tensor_1d(compute_ctx, GGML_TYPE_I32, static_cast(pos_embed_idx_data[index].size())); + set_backend_tensor_data(idx_tensor, pos_embed_idx_data[index].data()); + auto embed = pos_embed->forward(runner_ctx, idx_tensor); + embed = ggml_reshape_2d(compute_ctx, embed, embed->ne[0], embed->ne[1] * embed->ne[2]); + auto weight_tensor = ggml_new_tensor_2d(compute_ctx, GGML_TYPE_F32, 1, static_cast(pos_embed_weight_data[index].size())); + set_backend_tensor_data(weight_tensor, pos_embed_weight_data[index].data()); + embed = ggml_mul(compute_ctx, embed, weight_tensor); + result = result == nullptr ? embed : ggml_add(compute_ctx, result, embed); + } + return result; + } + ggml_cgraph* build_encode_image_graph(const sd::Tensor& pixel_values_tensor, int64_t image_width, int64_t image_height, @@ -2070,6 +2230,9 @@ namespace LLM { const int grid_h = static_cast(image_height) / params.vision.patch_size; const int grid_w = static_cast(image_width) / params.vision.patch_size; const int head_dim = static_cast(params.vision.hidden_size / params.vision.num_heads); + auto runner_ctx = get_context(); + auto vision = model.vision_model(); + auto pos_embeds = build_qwen3_vl_patch_pos_embeds(&runner_ctx, vision, grid_h, grid_w); window_index_vec.resize(static_cast((grid_h / params.vision.spatial_merge_size) * (grid_w / params.vision.spatial_merge_size))); for (size_t index = 0; index < window_index_vec.size(); ++index) { @@ -2084,13 +2247,13 @@ namespace LLM { const int pos_len = static_cast(pe_vec.size() / head_dim / 2); auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, head_dim / 2, pos_len); set_backend_tensor_data(pe, pe_vec.data()); - auto runner_ctx = get_context(); auto outputs = model.vision_forward_outputs(&runner_ctx, pixel_values, pe, nullptr, nullptr, - nullptr); + nullptr, + pos_embeds); ggml_build_forward_expand(gf, outputs[0]); return gf; } @@ -2333,13 +2496,15 @@ namespace LLM { const int grid_h = static_cast(image_height / params.vision.patch_size); const int grid_w = static_cast(image_width / params.vision.patch_size); const int head_dim = static_cast(params.vision.hidden_size / params.vision.num_heads); + auto runner_ctx = get_context(); + auto vision = model.vision_model(); + auto pos_embeds = build_qwen3_vl_patch_pos_embeds(&runner_ctx, vision, grid_h, grid_w); window_index_vec.resize(static_cast((grid_h / params.vision.spatial_merge_size) * (grid_w / params.vision.spatial_merge_size))); for (size_t index = 0; index < window_index_vec.size(); ++index) window_index_vec[index] = static_cast(index); pe_vec = Rope::gen_qwen2vl_pe(grid_h, grid_w, params.vision.spatial_merge_size, window_index_vec, 10000, {head_dim / 2, head_dim / 2}); auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, head_dim / 2, static_cast(pe_vec.size() / head_dim / 2)); set_backend_tensor_data(pe, pe_vec.data()); - auto runner_ctx = get_context(); - auto outputs = model.vision_forward_outputs(&runner_ctx, pixels, pe, nullptr, nullptr, nullptr); + auto outputs = model.vision_forward_outputs(&runner_ctx, pixels, pe, nullptr, nullptr, nullptr, pos_embeds); auto combined = outputs[0]; for (size_t index = 1; index < outputs.size(); ++index) combined = ggml_concat(compute_ctx, combined, outputs[index], 0); ggml_build_forward_expand(graph, combined); @@ -2355,6 +2520,63 @@ namespace LLM { } return outputs; } + + std::vector> encode_video_block_outputs(const int n_threads, + const sd::Tensor& frames) { + if (params.vision.arch != LLMVisionArch::QWEN3_VL) { + return encode_image_outputs(n_threads, frames); + } + const int grid_h = static_cast(frames.shape()[1] / params.vision.patch_size); + const int grid_w = static_cast(frames.shape()[0] / params.vision.patch_size); + const auto pixel_values = process_video_block_patches_host(frames); + if (pixel_values.empty()) { + return {}; + } + auto get_graph = [&]() -> ggml_cgraph* { + ggml_cgraph* graph = new_graph_custom(LLM_GRAPH_SIZE); + auto pixels = make_input(pixel_values); + auto runner_ctx = get_context(); + auto vision = model.vision_model(); + const int head_dim = static_cast(params.vision.hidden_size / params.vision.num_heads); + auto pos_embeds = build_qwen3_vl_patch_pos_embeds(&runner_ctx, vision, grid_h, grid_w); + window_index_vec.resize(static_cast((grid_h / params.vision.spatial_merge_size) * + (grid_w / params.vision.spatial_merge_size))); + for (size_t index = 0; index < window_index_vec.size(); ++index) { + window_index_vec[index] = static_cast(index); + } + pe_vec = Rope::gen_qwen2vl_pe(grid_h, + grid_w, + params.vision.spatial_merge_size, + window_index_vec, + 10000, + {head_dim / 2, head_dim / 2}); + const int pos_len = static_cast(pe_vec.size() / head_dim / 2); + auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, head_dim / 2, pos_len); + set_backend_tensor_data(pe, pe_vec.data()); + auto outputs = model.vision_forward_outputs(&runner_ctx, + pixels, + pe, + nullptr, + nullptr, + nullptr, + pos_embeds); + auto combined = outputs[0]; + for (size_t index = 1; index < outputs.size(); ++index) { + combined = ggml_concat(compute_ctx, combined, outputs[index], 0); + } + ggml_build_forward_expand(graph, combined); + return graph; + }; + auto combined = take_or_empty(GGMLRunner::compute(get_graph, n_threads, true)); + if (combined.empty()) return {}; + const size_t count = params.vision.deepstack_visual_indexes.size() + 1; + std::vector> outputs; + outputs.reserve(count); + for (size_t index = 0; index < count; ++index) { + outputs.push_back(sd::ops::slice(combined, 0, static_cast(index) * params.hidden_size, static_cast(index + 1) * params.hidden_size)); + } + return outputs; + } }; struct LLMEmbedder { diff --git a/src/dit_models/pipelines/minimax_h3_pipeline.cpp b/src/dit_models/pipelines/minimax_h3_pipeline.cpp index eb5b68e..d181b27 100644 --- a/src/dit_models/pipelines/minimax_h3_pipeline.cpp +++ b/src/dit_models/pipelines/minimax_h3_pipeline.cpp @@ -365,6 +365,8 @@ bool MiniMaxH3Pipeline::build_text_context(const char* prompt, std::vector>> image_embeds; std::vector image_embed_infos; std::vector>>> deepstack_image_embeds(3); + std::vector> vision_token_ranges; + const int vision_patch_size = conditioner_->model.params.vision.patch_size; for (int image_index = 0; image_index < ref_image_count; ++image_index) { const ed_image_t& image = ref_images[image_index]; int width = 0; @@ -397,7 +399,9 @@ bool MiniMaxH3Pipeline::build_text_context(const char* prompt, for (size_t layer = 0; layer < deepstack_image_embeds.size(); ++layer) { deepstack_image_embeds[layer].emplace_back(static_cast(prefix_tokens.size()), std::move(image_outputs[layer + 1])); } - image_embed_infos.push_back({static_cast(prefix_tokens.size()), image_tokens, 1, height / 16, width / 16}); + const int embed_index = static_cast(prefix_tokens.size()); + image_embed_infos.push_back({embed_index, image_tokens, 1, height / vision_patch_size, width / vision_patch_size}); + vision_token_ranges.emplace_back(embed_index, image_tokens); } int video_number = 0; int audio_number = 0; @@ -418,9 +422,17 @@ bool MiniMaxH3Pipeline::build_text_context(const char* prompt, } presentations += "