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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ API and command-line option may change frequently.***
- [Z-Image](./docs/z_image.md)
- [Z-Image L2P](./docs/z_image_l2p.md)
- [MiniT2I](./docs/minit2i.md)
- [P-H3-Image](./docs/p_h3_image.md)
- [SenseNova U1.5](./docs/sensenova_u1.md)
- [Ovis-Image](./docs/ovis_image.md)
- [Anima](./docs/anima.md)
Expand Down
34 changes: 34 additions & 0 deletions docs/p_h3_image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# How to Use

P-H3-Image supports text-to-image and reference-image editing. It uses a MiniMax-H3 diffusion transformer, a single-frame VAE, and Qwen3-VL-4B with a text connector. Prompt enhancement is not included.

## Download weights

- Download P-H3-Image
- safetensors: https://huggingface.co/PrunaAI/P-H3-Image-Comfy/tree/main/diffusion_models
- Download P-H3-Image VAE
- safetensors: https://huggingface.co/PrunaAI/P-H3-Image-Comfy/tree/main/vae
- Download Qwen3-VL-4B with the text connector
- https://huggingface.co/PrunaAI/P-H3-Image-Comfy/tree/main/text_encoders

Alternatively, reuse Qwen3-VL-4B, including its vision tower, with the [separate text connector](https://huggingface.co/PrunaAI/P-H3-Image/blob/main/text_connector/diffusion_pytorch_model.safetensors). Save the connector as `p-h3-image_text_connector.safetensors` and pass it with `--embeddings-connectors`.

Use the P-H3 image VAE; the MiniMax-H3 video VAE is not compatible. No audio VAE is needed.

## Examples

### Text-to-image

```
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\p-h3-image_int8_convrot.safetensors --llm ..\models\text_encoders\qwen3vl_4b_bf16.safetensors --embeddings-connectors ..\models\text_encoders\p-h3-image_text_connector.safetensors --vae ..\models\vae\p-h3-image_vae_bf16.safetensors --cfg-scale 1.0 --steps 8 --sampling-method euler --sigmas p-h3-image -p "a lovely cat holding a sign says 'p-h3-image.cpp'" -v --offload-to-cpu --rng cpu -H 2912 -W 2912
```

### Image editing

```
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\p-h3-image_int8_convrot.safetensors --llm ..\models\text_encoders\qwen3vl_4b_bf16.safetensors --embeddings-connectors ..\models\text_encoders\p-h3-image_text_connector.safetensors --vae ..\models\vae\p-h3-image_vae_bf16.safetensors --cfg-scale 1.0 --steps 8 --sampling-method euler --sigmas p-h3-image -r source.png -r reference.png -p "Edit <Picture 1>. Give the cat the scarf from <Picture 2>, preserving its face and the background." -v --offload-to-cpu --rng cpu -H 2912 -W 2912
```

Reference images are numbered in argument order as `<Picture 1>`, `<Picture 2>`, and so on. A plain edit instruction gets `Edit <Picture 1>. ` prepended automatically. Prompts starting with `Edit <Picture N>` or `subject_definitions:` are used directly.

Use dimensions divisible by 32. The upstream default is 2912 × 2912; smaller canvases reduce memory use. Use CFG 1 and `--sigmas p-h3-image` for the upstream 8-step schedule. Keep the default VAE tiling size of 256 pixels.
8 changes: 5 additions & 3 deletions examples/common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ ArgOptions SDContextParams::get_options() {
&uncond_diffusion_model_path},
{"",
"--embeddings-connectors",
"path to LTXAV embeddings connectors",
"path to embeddings connectors (LTXAV or P-H3-Image)",
0,
&embeddings_connectors_path},
{"",
Expand Down Expand Up @@ -1530,6 +1530,8 @@ ArgOptions SDGenerationParams::get_options() {
std::string sigmas_str = value;
if (sigmas_str == "qwen-image-2.1-turbo") {
sigmas_str = "1.0,0.978453,0.95418,0.926626,0.89508,0.845148,0.704534,0.414568,0.0";
} else if (sigmas_str == "p-h3-image") {
sigmas_str = "1.0,0.984753,0.967905,0.951688,0.925372,0.881923,0.810083,0.528718,0.0";
}
if (!sigmas_str.empty() && sigmas_str.front() == '[') {
sigmas_str.erase(0, 1);
Expand Down Expand Up @@ -1777,11 +1779,11 @@ ArgOptions SDGenerationParams::get_options() {
on_scheduler_arg},
{"",
"--sigmas",
"custom sigma values for the sampler, comma-separated (e.g., \"14.61,7.8,3.5,0.0\") or preset: qwen-image-2.1-turbo.",
"custom sigma values for the sampler, comma-separated (e.g., \"14.61,7.8,3.5,0.0\") or preset: qwen-image-2.1-turbo, p-h3-image.",
on_sigmas_arg},
{"",
"--hires-sigmas",
"custom sigma values for the highres fix second pass, comma-separated (e.g., \"0.85,0.725,0.421875,0.0\") or preset: qwen-image-2.1-turbo.",
"custom sigma values for the highres fix second pass, comma-separated (e.g., \"0.85,0.725,0.421875,0.0\") or preset: qwen-image-2.1-turbo, p-h3-image.",
on_hires_sigmas_arg},
{"",
"--skip-layers",
Expand Down
1 change: 1 addition & 0 deletions examples/server/recipes/defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ For community edit adapters and other optional extensions, these are the base-mo
| `minit2i` | 2.5 | 20 | 512 × 512 | [Source](https://huggingface.co/MiniT2I/MiniT2I/raw/main/pipeline.py) B/16 example and native model size; L/16 uses CFG 6. |
| `moss-image-base` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base) |
| `moss-image-sft` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT) |
| `p-h3-image` | 1 | 8 | 1024 × 1024 | [Runtime guide](../../../docs/p_h3_image.md) Fixed P-H3 sigma schedule; upstream 2912 × 2912 reduced to the catalog default. |
| `nitrosd-realism` | 1 | 1 | 1024 × 1024 | [Source](https://huggingface.co/ChenDY/NitroFusion) |
| `ovis-image` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/ATH-MaaS/Ovis-Image-7B) |
| `pid` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. |
Expand Down
55 changes: 55 additions & 0 deletions examples/server/recipes/p-h3-image.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"schema_version": 1,
"id": "p-h3-image",
"name": "P-H3-Image",
"size_multiple": 32,
"description": "Supports text-to-image and reference-image editing with the fixed 8-step schedule. Use the P-H3 single-frame VAE and Qwen3-VL-4B with its vision tower. Supply the P-H3 text connector separately unless it is included in the text encoder.",
"model_url": "https://huggingface.co/PrunaAI/P-H3-Image",
"slots": [
{
"id": "diffusion_model",
"required": true
},
{
"id": "llm",
"required": true,
"label": "Qwen 3 VL 4B Text Encoder"
},
{
"id": "llm_vision",
"required": false,
"label": "Vision projector (required when not included in the text encoder)"
},
{
"id": "embeddings_connectors",
"required": false,
"label": "P-H3 Text Connector (required when not included in the text encoder)"
},
{
"id": "vae",
"required": true,
"label": "P-H3 Image VAE"
}
],
"defaults": {
"sampling_method": "euler",
"cfg_scale": 1,
"steps": 8,
"width": 1024,
"height": 1024,
"custom_sigmas": [
1.0,
0.984753,
0.967905,
0.951688,
0.925372,
0.881923,
0.810083,
0.528718,
0.0
]
},
"features": [
"ref_images"
]
}
64 changes: 55 additions & 9 deletions src/conditioning/conditioner.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "model/te/llada_image_te.hpp"
#include "model/te/llm.hpp"
#include "model/te/ming_image_te.hpp"
#include "model/te/ph3_image_connector.hpp"
#include "model/te/t5.hpp"
#include "model_loader.h"
#include "tokenizers/sensenova_u1_tokenizer.h"
Expand Down Expand Up @@ -1976,6 +1977,7 @@ struct LLMEmbedder : public Conditioner {
std::shared_ptr<Tokenizer> tokenizer;
std::shared_ptr<LLM::LLMRunner> llm;
std::shared_ptr<T5Runner> byt5;
std::shared_ptr<PH3ImageTextConnectorRunner> ph3_connector;

LLMEmbedder(ggml_backend_t backend,
const String2TensorStorage& tensor_storage_map = {},
Expand Down Expand Up @@ -2022,12 +2024,18 @@ struct LLMEmbedder : public Conditioner {
} else if (version == VERSION_MOSS_IMAGE) {
arch = LLM::LLMArch::QWEN3_5;
}
llm = std::make_shared<LLM::LLMRunner>(arch,
backend,
tensor_storage_map,
"text_encoders.llm",
enable_vision,
weight_manager);
llm = std::make_shared<LLM::LLMRunner>(arch,
backend,
tensor_storage_map,
"text_encoders.llm",
enable_vision,
weight_manager);
if (version == VERSION_P_H3_IMAGE) {
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");
}
ph3_connector = std::make_shared<PH3ImageTextConnectorRunner>(backend, tensor_storage_map, weight_manager);
}
int pad_id = 151643;
if (arch == LLM::LLMArch::MISTRAL_SMALL_3_2 || arch == LLM::LLMArch::MINISTRAL_3_3B) {
pad_id = 11;
Expand Down Expand Up @@ -2063,6 +2071,9 @@ struct LLMEmbedder : public Conditioner {

void get_param_tensors(std::map<std::string, ggml_tensor*>& tensors) override {
llm->get_param_tensors(tensors, "text_encoders.llm");
if (ph3_connector) {
ph3_connector->get_param_tensors(tensors);
}
if (byt5) {
byt5->get_param_tensors(tensors, "text_encoders.t5xxl.transformer");
}
Expand All @@ -2074,13 +2085,19 @@ struct LLMEmbedder : public Conditioner {

void set_max_graph_vram_bytes(size_t max_vram_bytes) override {
llm->set_max_graph_vram_bytes(max_vram_bytes);
if (ph3_connector) {
ph3_connector->set_max_graph_vram_bytes(max_vram_bytes);
}
if (byt5) {
byt5->set_max_graph_vram_bytes(max_vram_bytes);
}
}

void set_runtime_backends(const std::vector<ggml_backend_t>& backends) override {
llm->set_runtime_backends(backends);
if (ph3_connector) {
ph3_connector->set_runtime_backends(backends);
}
if (byt5) {
byt5->set_runtime_backends(backends);
}
Expand Down Expand Up @@ -2113,19 +2130,28 @@ struct LLMEmbedder : public Conditioner {

void set_flash_attention_enabled(bool enabled) override {
llm->set_flash_attention_enabled(enabled);
if (ph3_connector) {
ph3_connector->set_flash_attention_enabled(enabled);
}
if (byt5) {
byt5->set_flash_attention_enabled(enabled);
}
}

void set_scale_overrides(float linear_scale, float attn_scale) override {
llm->set_scale_overrides(linear_scale, attn_scale);
if (ph3_connector) {
ph3_connector->set_scale_overrides(linear_scale, attn_scale);
}
if (byt5) {
byt5->set_scale_overrides(linear_scale, attn_scale);
}
}

void set_weight_adapter(const std::shared_ptr<WeightAdapter>& adapter) override {
if (ph3_connector) {
ph3_connector->set_weight_adapter(adapter);
}
if (llm) {
llm->set_weight_adapter(adapter);
}
Expand All @@ -2135,6 +2161,9 @@ struct LLMEmbedder : public Conditioner {
}

void runner_end() override {
if (ph3_connector) {
ph3_connector->runner_end();
}
if (llm) {
llm->runner_end();
}
Expand Down Expand Up @@ -2311,6 +2340,12 @@ struct LLMEmbedder : public Conditioner {
LOG_ERROR("LLM prompt encoding failed");
return {};
}
if (ph3_connector) {
hidden_states = ph3_connector->compute(n_threads, hidden_states);
if (hidden_states.empty()) {
return {};
}
}
hidden_states = apply_token_weights(std::move(hidden_states), weights);
GGML_ASSERT(hidden_states.shape()[1] > prompt_template_encode_start_idx);

Expand Down Expand Up @@ -2457,8 +2492,9 @@ struct LLMEmbedder : public Conditioner {

if (sd_version_is_minimax_h3(version)) {
prompt_template_encode_start_idx = 0;
out_layers = {50};
prompt_attn_range = {0, 0};
// The final tap is normalized; LLMRunner exposes it at num_layers + 1.
out_layers = version == VERSION_P_H3_IMAGE ? std::set<int>{13, 19, 25, 31, 37} : std::set<int>{50};
prompt_attn_range = {0, 0};

if (llm->enable_vision) {
const int patch_size = llm->config.vision.patch_size;
Expand Down Expand Up @@ -2564,7 +2600,17 @@ struct LLMEmbedder : public Conditioner {
}
}
}
prompt += conditioner_params.text;
auto text = conditioner_params.text;
if (version == VERSION_P_H3_IMAGE && !image_embeds.empty()) {
// Preserve explicit picture targets and structured briefs from the upstream upsampler.
static const std::regex verbatim(R"(^\s*(Edit <Picture \d+>|subject_definitions:))");
if (!std::regex_search(text, verbatim)) {
text = "Edit <Picture 1>. " + text;
} else {
text = trim(text);
}
}
prompt += text;
} else if (sd_version_is_hunyuan_video(version)) {
prompt_template_encode_start_idx = 98;
out_layers = {26};
Expand Down
2 changes: 1 addition & 1 deletion src/core/layer_split_partition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ namespace sd {
for (size_t seg_idx = 0; seg_idx < plan.segments.size(); seg_idx++) {
ggml_backend_t backend = backend_by_segment[seg_idx];
const auto& segment = plan.segments[seg_idx];
auto assign_node = [&](int node_index) {
auto assign_node = [&](int node_index) {
if (node_index < 0 || node_index >= n_nodes) {
return;
}
Expand Down
19 changes: 14 additions & 5 deletions src/core/vulkan/shaders/modulate.comp
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
#version 450

layout(local_size_x = 256) in;
layout(binding = 0) readonly buffer X { float x[]; };
layout(binding = 1) readonly buffer S { float scale[]; };
layout(binding = 2) readonly buffer H { float shift[]; };
layout(binding = 3) writeonly buffer D { float dst[]; };
layout(binding = 0) readonly buffer X {
float x[];
};
layout(binding = 1) readonly buffer S {
float scale[];
};
layout(binding = 2) readonly buffer H {
float shift[];
};
layout(binding = 3) writeonly buffer D {
float dst[];
};
layout(push_constant) uniform Parameters {
uint ne[4], scale_ne[4], shift_ne[4];
uint x_stride[4], scale_stride[4], shift_stride[4];
uint offsets[4];
uint count, has_shift;
} p;
}
p;

void main() {
uint group = gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x;
Expand Down
4 changes: 2 additions & 2 deletions src/core/vulkan/shaders/modulate_convrot.comp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

#extension GL_EXT_control_flow_attributes : require

layout(constant_id = 0) const uint K = 4096;
layout(constant_id = 1) const bool AFFINE = false;
layout(constant_id = 0) const uint K = 4096;
layout(constant_id = 1) const bool AFFINE = false;
layout(constant_id = 2) const bool HAS_SHIFT = true;
layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in;
layout(binding = 0) readonly buffer X {
Expand Down
3 changes: 2 additions & 1 deletion src/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ enum SDVersion {
VERSION_MOSS_IMAGE,
VERSION_FIBO2,
VERSION_CHROMA,
VERSION_P_H3_IMAGE,
VERSION_COUNT,
};

Expand Down Expand Up @@ -166,7 +167,7 @@ static inline bool sd_version_is_ltxav(SDVersion version) {
}

static inline bool sd_version_is_minimax_h3(SDVersion version) {
return version == VERSION_MINIMAX_H3;
return version == VERSION_MINIMAX_H3 || version == VERSION_P_H3_IMAGE;
}

static inline bool sd_version_is_wan(SDVersion version) {
Expand Down
9 changes: 9 additions & 0 deletions src/model/integrations/minimax_h3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ const std::vector<ModelDefinition>& minimax_h3_definitions() {
definition.build = build_minimax_h3;
result.push_back(std::move(definition));
}
{
ModelDefinition definition = result.front();
definition.version = VERSION_P_H3_IMAGE;
definition.name = "P-H3-Image";
definition.traits.supports_video_generation = false;
definition.sampling.default_flow_shift = 1.f;
definition.ref_image_preset = "qwen";
result.push_back(std::move(definition));
}
return result;
}();
return definitions;
Expand Down
Loading
Loading