From 8b1e9df27dd15f7b28ec3638bbcc56f8bd53c6d2 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 12 Nov 2026 16:20:18 +0100 Subject: [PATCH] feat: allow the P-H3-Image text encoder for MiniMax-H3-Image (#2337) --- docs/minimax_h3_image.md | 19 ++++++++++++++++++- examples/server/recipes/minimax-h3-image.json | 9 +++++++-- src/conditioning/conditioner.hpp | 15 ++++++++++++--- src/name_conversion.cpp | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/minimax_h3_image.md b/docs/minimax_h3_image.md index 0355663d..0a980cd9 100644 --- a/docs/minimax_h3_image.md +++ b/docs/minimax_h3_image.md @@ -16,10 +16,19 @@ Use the MiniMax-H3 text encoder, including its Qwen3-VL vision tower and DeepStack mergers. An existing MiniMax-H3 text encoder can be reused. If the vision tower is stored separately, pass it with `--llm_vision`. +The P-H3-Image text encoder can be used instead: Qwen3-VL-4B with the P-H3 +text connector. It needs much less memory (about 5 GB for the INT8 ConvRot +file, versus 18 GB for the 32B encoder in Q4_K_M) and works for both +text-to-image and reference-image editing. Pass the P-H3-Image text encoder, +which includes the connector, with `--llm`. Alternatively, pass Qwen3-VL-4B +with `--llm` and the [separate text connector](https://huggingface.co/PrunaAI/P-H3-Image/blob/main/text_connector/diffusion_pytorch_model.safetensors) +with `--embeddings-connectors`. Reference-image editing needs the Qwen3-VL-4B +vision tower in the text encoder or in `--llm_vision`. + Use a single-file diffusion checkpoint with the six-step PDD output heads included, such as `minimax_h3_image_pruned_bf16.safetensors` or its INT8 ConvRot version. Use the MiniMax-H3-Image VAE; the MiniMax-H3 video VAE and P-H3-Image -VAE are incompatible. No audio VAE or P-H3 text connector is needed. +VAE are incompatible. No audio VAE is needed. ### Download weights @@ -28,6 +37,8 @@ VAE are incompatible. No audio VAE or P-H3 text connector is needed. - Download qwen3vl_32b_minimax_h3 - safetensors: https://huggingface.co/Comfy-Org/MiniMax-H3-Image/tree/main/text_encoders - gguf: https://huggingface.co/leejet/MiniMax-H3-GGUF/tree/main +- Or download the P-H3-Image text encoder (Qwen3-VL-4B with the text connector) + - safetensors: https://huggingface.co/PrunaAI/P-H3-Image-Comfy/tree/main/text_encoders - Download vae - safetensors: https://huggingface.co/Comfy-Org/MiniMax-H3-Image/tree/main/vae @@ -37,6 +48,12 @@ VAE are incompatible. No audio VAE or P-H3 text connector is needed. .\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\minimax_h3_image_pruned_bf16.safetensors --vae ..\models\vae\minimax_h3_image_vae_fp16.safetensors --llm ..\models\text_encoders\qwen3vl_32b_minimax_h3-Q4_K_M.gguf -p "A silver tabby kitten holding a sign reading 'sd.cpp', natural daylight" --cfg-scale 1.0 --steps 6 --sampling-method euler --sigmas minimax-h3-image -v -W 1024 -H 1024 --offload-to-cpu --rng cpu --diffusion-fa --vae-tiling ``` +With the P-H3-Image text encoder, replace the `--llm` argument: + +```sh +--llm ..\models\text_encoders\p-h3-image_text_encoder_int8_convrot.safetensors +``` + Use Euler, CFG 1, and `--sigmas minimax-h3-image` for the model's six-step schedule. The default discrete scheduler also produces this schedule with `--steps 6`. Prompt expansion is not included. diff --git a/examples/server/recipes/minimax-h3-image.json b/examples/server/recipes/minimax-h3-image.json index 709cd4e5..0774b5b1 100644 --- a/examples/server/recipes/minimax-h3-image.json +++ b/examples/server/recipes/minimax-h3-image.json @@ -3,7 +3,7 @@ "id": "minimax-h3-image", "name": "MiniMax H3 Image", "size_multiple": 32, - "description": "Text-to-image and reference-image editing with the six-step PDD checkpoint. Requires the MiniMax-H3-Image 2D VAE and MiniMax-H3 Qwen3-VL-32B text encoder with its vision tower.", + "description": "Text-to-image and reference-image editing with the six-step PDD checkpoint. Requires the MiniMax-H3-Image 2D VAE and either the MiniMax-H3 Qwen3-VL-32B text encoder with its vision tower or the smaller P-H3-Image text encoder (Qwen3-VL-4B with the P-H3 text connector).", "model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3-Image", "slots": [ { @@ -13,13 +13,18 @@ { "id": "llm", "required": true, - "label": "MiniMax-H3 Qwen 3 VL 32B Text Encoder" + "label": "MiniMax-H3 Qwen 3 VL 32B or P-H3 Qwen 3 VL 4B Text Encoder" }, { "id": "llm_vision", "required": false, "label": "Vision tower (required when not included in the text encoder)" }, + { + "id": "embeddings_connectors", + "required": false, + "label": "P-H3 Text Connector (for Qwen 3 VL 4B, when not included in the text encoder)" + }, { "id": "vae", "required": true, diff --git a/src/conditioning/conditioner.hpp b/src/conditioning/conditioner.hpp index 12f4dc9a..babf98e0 100644 --- a/src/conditioning/conditioner.hpp +++ b/src/conditioning/conditioner.hpp @@ -2030,11 +2030,20 @@ struct LLMEmbedder : public Conditioner { "text_encoders.llm", enable_vision, weight_manager); - if (version == VERSION_P_H3_IMAGE) { + // MiniMax-H3-Image also accepts the P-H3-Image text encoder in place of the MiniMax-H3 Qwen3-VL-32B encoder. + const bool has_ph3_connector = tensor_storage_map.find(std::string(PH3ImageTextConnectorRunner::prefix) + ".proj.weight") != + tensor_storage_map.end(); + if (version == VERSION_P_H3_IMAGE || (version == VERSION_MINIMAX_H3_IMAGE && has_ph3_connector)) { if (llm->config.num_layers != 36 || llm->config.hidden_size != 2560) { - throw std::runtime_error("P-H3-Image requires the Qwen3-VL-4B text encoder"); + throw std::runtime_error(version == VERSION_P_H3_IMAGE + ? "P-H3-Image requires the Qwen3-VL-4B text encoder" + : "The P-H3-Image text connector requires the Qwen3-VL-4B text encoder"); } ph3_connector = std::make_shared(backend, tensor_storage_map, weight_manager); + } else if (version == VERSION_MINIMAX_H3_IMAGE && llm->config.hidden_size == 2560) { + throw std::runtime_error( + "MiniMax-H3-Image with Qwen3-VL-4B requires the P-H3-Image text connector; " + "pass it with --embeddings-connectors or use the MiniMax-H3 Qwen3-VL-32B text encoder"); } int pad_id = 151643; if (arch == LLM::LLMArch::MISTRAL_SMALL_3_2 || arch == LLM::LLMArch::MINISTRAL_3_3B) { @@ -2493,7 +2502,7 @@ struct LLMEmbedder : public Conditioner { if (sd_version_is_minimax_h3(version)) { prompt_template_encode_start_idx = 0; // The final tap is normalized; LLMRunner exposes it at num_layers + 1. - out_layers = version == VERSION_P_H3_IMAGE ? std::set{13, 19, 25, 31, 37} : std::set{50}; + out_layers = ph3_connector ? std::set{13, 19, 25, 31, 37} : std::set{50}; prompt_attn_range = {0, 0}; if (llm->enable_vision) { diff --git a/src/name_conversion.cpp b/src/name_conversion.cpp index 79179f64..d140a54e 100644 --- a/src/name_conversion.cpp +++ b/src/name_conversion.cpp @@ -1741,7 +1741,7 @@ std::string convert_tensor_name(std::string name, SDVersion version) { const std::vector> connector_prefix_map = { {"embeddings_connectors.text_encoders.", "text_encoders."}, - {"embeddings_connectors.", version == VERSION_P_H3_IMAGE ? "text_encoders.ph3_image_connector." : ""}, + {"embeddings_connectors.", sd_version_is_minimax_h3_image(version) ? "text_encoders.ph3_image_connector." : ""}, }; replace_with_prefix_map(name, connector_prefix_map);