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 @@ -64,6 +64,7 @@ API and command-line option may change frequently.***
- [HiDream-O1-Image](./docs/hidream_o1_image.md)
- [Ideogram4](./docs/ideogram4.md)
- [LLaDA-Image](./docs/llada_image.md)
- [PixArt](./docs/pixart.md)
- [Image Edit Models](./docs/edit.md)
- [FLUX.1-Kontext-dev](./docs/kontext.md)
- [Qwen Image Edit series](./docs/qwen_image_edit.md)
Expand Down
46 changes: 46 additions & 0 deletions docs/pixart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# How to Use

You can run PixArt-α / PixArt-Σ with stable-diffusion.cpp.

PixArt is a DiT-based text-to-image model family conditioned by a T5-XXL text
encoder and a 4-channel VAE: SDXL-style for PixArt-Σ and SD1.x-style for PixArt-α.

## Download weights

- Download the transformer (diffusion model)
- PixArt-Σ XL-2 1024-MS: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/transformer
- PixArt-α XL-2 1024-MS: https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/tree/main/transformer
- Download the T5-XXL text encoder
- safetensors: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/text_encoder
- Download the VAE
- PixArt-Σ: https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/tree/main/vae
- PixArt-α: https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/tree/main/vae
- Use the VAE matching the checkpoint's latent space. For TAE decoding or
preview, use TAESDXL for PixArt-Σ and TAESD for PixArt-α.
- Tokenizer: the T5 vocabulary is embedded; no extra tokenizer file is needed.

## Examples

```
.\bin\Release\sd-cli.exe --diffusion-model ..\models\diffusion_models\pixart_sigma_xl2_1024_ms.safetensors --t5xxl ..\models\text_encoders\t5xxl.safetensors --vae ..\models\vae\pixart_vae.safetensors -p "a lovely cat" --cfg-scale 4.5 -W 1024 -H 1024 --steps 20 -v
```

## Notes

- The VAE scaling factor defaults to `0.13025` for PixArt-Σ. PixArt-α
checkpoints with resolution micro-condition weights use `0.18215`.
PixArt-α 512 has the same tensor layout as PixArt-Σ, so it requires an
explicit override: `--model-args "pixart_vae_scale_factor=0.18215"`.
This argument can also override the scale for other compatible checkpoints.
- PixArt-Σ checkpoints compute 2D sincos positional embeddings at runtime;
the trained grid is 64x64 patches with an interpolation scale of 2.
For checkpoints trained at a different resolution, the positional embedding
parameters can be adjusted via model args:
`--model-args "pixart_pos_embed_base_size=<trained grid>,pixart_interpolation_scale=<scale>"`
(e.g. `pixart_pos_embed_base_size=32,pixart_interpolation_scale=1,pixart_vae_scale_factor=0.18215` for
PixArt-α XL-2 512).
- Checkpoints carrying resolution/aspect-ratio micro-condition weights are
detected but those conditions are not applied yet; a warning is logged and
generation proceeds with the timestep embedding only.
- The transformer predicts 8 channels (noise + learned variance); only the
noise half is used for sampling, matching the reference implementation.
8 changes: 7 additions & 1 deletion src/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum SDVersion {
VERSION_SENSENOVA_U1_5,
VERSION_LLADA_IMAGE,
VERSION_ESRGAN,
VERSION_PIXART,
VERSION_COUNT,
};

Expand Down Expand Up @@ -252,6 +253,10 @@ static inline bool sd_version_is_sensenova_u1(SDVersion version) {
return version == VERSION_SENSENOVA_U1_5;
}

static inline bool sd_version_is_pixart(SDVersion version) {
return version == VERSION_PIXART;
}

static inline bool sd_version_supports_video_generation(SDVersion version) {
return version == VERSION_SVD || sd_version_is_wan(version) || sd_version_is_hunyuan_video(version) || sd_version_is_lingbot_video(version) || sd_version_is_ltxav(version) || sd_version_is_minimax_h3(version);
}
Expand Down Expand Up @@ -320,7 +325,8 @@ static inline bool sd_version_is_dit(SDVersion version) {
sd_version_is_sefi_image(version) ||
sd_version_is_krea2(version) ||
sd_version_is_mage_flow(version) ||
sd_version_is_sensenova_u1(version)) {
sd_version_is_sensenova_u1(version) ||
sd_version_is_pixart(version)) {
return true;
}
return false;
Expand Down
390 changes: 390 additions & 0 deletions src/model/diffusion/pixart.hpp

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/model/vae/auto_encoder_kl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ class AutoEncoderKLModel : public GGMLBlock {
const String2TensorStorage& tensor_storage_map = {},
const std::string& prefix = "")
: version(version), decode_only(decode_only), use_video_decoder(use_video_decoder) {
if (sd_version_is_dit(version)) {
if (sd_version_is_dit(version) && version != VERSION_PIXART) {
if (sd_version_uses_flux2_vae(version)) {
dd_config.z_channels = 32;
embed_dim = 32;
Expand Down Expand Up @@ -678,7 +678,7 @@ struct AutoEncoderKL : public VAE {
if (sd_version_is_sd1(version) || sd_version_is_sd2(version)) {
scale_factor = 0.18215f;
shift_factor = 0.f;
} else if (sd_version_is_sdxl(version)) {
} else if (sd_version_is_sdxl(version) || sd_version_is_pixart(version)) {
scale_factor = 0.13025f;
shift_factor = 0.f;
} else if (sd_version_is_sd3(version)) {
Expand Down
2 changes: 1 addition & 1 deletion src/model/vae/tae.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ class TAESD : public GGMLBlock {
bool use_midblock_gn = false;
taef2 = sd_version_uses_flux2_vae(version);

if (sd_version_is_dit(version)) {
if (sd_version_is_dit(version) && !sd_version_is_pixart(version)) {
z_channels = 16;
}
if (taef2) {
Expand Down
7 changes: 7 additions & 0 deletions src/model_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ SDVersion ModelLoader::get_sd_version() const {
return VERSION_ERNIE_IMAGE;
}
if (tensor_storage.name.find("model.diffusion_model.adaln_single.emb.timestep_embedder.linear_1.bias") != std::string::npos) {
// PixArt shares this signature with LTX-AV; pos_embed.proj is PixArt-only.
if (tensor_storage_map.find("model.diffusion_model.pos_embed.proj.weight") != tensor_storage_map.end()) {
return VERSION_PIXART;
}
return VERSION_LTXAV;
}
if (tensor_storage.name.find("model.diffusion_model.video_patch_proj.weight") != std::string::npos &&
Expand Down Expand Up @@ -1591,6 +1595,9 @@ bool ModelLoader::tensor_should_be_converted(const TensorStorage& tensor_storage
// Pass, do not convert. For Unet
} else if (contains(name, "embedding")) {
// Pass, do not convert embedding
} else if (contains(name, "scale_shift_table")) {
// Pass, do not convert. adaLN modulation tables (PixArt, LTXV) are sliced
// element-wise, which is invalid on quantized block layouts.
} else if (ends_with(name, "_pad_token")) {
// Pass, do not convert. LLaDA-Image stores its pad tokens far outside the f16
// range, so any format with an f16 scale or payload turns them into inf.
Expand Down
21 changes: 20 additions & 1 deletion src/pipeline/diffusion_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const char* model_version_to_str[] = {
"SenseNova U1.5",
"LLaDA-Image",
"ESRGAN",
"PixArt",
};

static_assert(VERSION_COUNT == sizeof(model_version_to_str) / sizeof(model_version_to_str[0]),
Expand All @@ -125,6 +126,18 @@ void calculate_alphas_cumprod(float* alphas_cumprod,
}
}

void calculate_alphas_cumprod_linear_beta(float* alphas_cumprod,
float beta_start,
float beta_end,
int timesteps = TIMESTEPS) {
float product = 1.0f;
for (int i = 0; i < timesteps; i++) {
float beta = beta_start + (beta_end - beta_start) * ((float)i / (timesteps - 1));
product *= 1.0f - beta;
alphas_cumprod[i] = product;
}
}

template <typename T, typename = void>
struct has_set_runtime_backends : std::false_type {};
template <typename T>
Expand Down Expand Up @@ -666,6 +679,10 @@ void StableDiffusionGGML::refresh_compvis_denoiser_sigmas() {
std::vector<float> alphas_cumprod(TIMESTEPS);
if (file_alphas_cumprod.size() == TIMESTEPS) {
alphas_cumprod = file_alphas_cumprod;
} else if (sd_version_is_pixart(version)) {
// PixArt checkpoints train with a linear beta schedule (0.0001 -> 0.02)
// instead of the scaled_linear schedule used by SD1.x/SDXL.
calculate_alphas_cumprod_linear_beta(alphas_cumprod.data(), 0.0001f, 0.02f);
} else {
calculate_alphas_cumprod(alphas_cumprod.data());
}
Expand Down Expand Up @@ -2731,7 +2748,7 @@ int StableDiffusionGGML::get_diffusion_model_down_factor() {
if (sd_version_is_dit(version)) {
if (sd_version_is_sensenova_u1(version)) {
down_factor = 32;
} else if (version == VERSION_QWEN_IMAGE_2_1 || sd_version_is_wan(version) || sd_version_is_lingbot_video(version) || sd_version_is_minimax_h3(version)) {
} else if (version == VERSION_QWEN_IMAGE_2_1 || sd_version_is_wan(version) || sd_version_is_lingbot_video(version) || sd_version_is_minimax_h3(version) || sd_version_is_pixart(version)) {
down_factor = 2;
} else {
down_factor = 1;
Expand Down Expand Up @@ -2769,6 +2786,8 @@ int StableDiffusionGGML::get_latent_channel() {
latent_channel = 128;
} else if (sd_version_is_mage_flow(version)) {
latent_channel = 128;
} else if (sd_version_is_pixart(version)) {
latent_channel = 4;
} else {
latent_channel = 16;
}
Expand Down
32 changes: 32 additions & 0 deletions src/pipeline/model_builders.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "model_builders.h"

#include <cmath>
#include <cstring>
#include <utility>

Expand Down Expand Up @@ -28,6 +29,7 @@
#include "model/diffusion/mmdit.hpp"
#include "model/diffusion/model.hpp"
#include "model/diffusion/pid.hpp"
#include "model/diffusion/pixart.hpp"
#include "model/diffusion/qwen_image.hpp"
#include "model/diffusion/qwen_image_2_1.hpp"
#include "model/diffusion/sensenova_u1.h"
Expand Down Expand Up @@ -301,6 +303,19 @@ namespace sd::model_builders {
weight_manager,
sd_ctx_params->model_args);
}
} else if (version == VERSION_PIXART) {
result.conditioner = std::make_shared<T5CLIPEmbedder>(ctx.backends.runtime_backend(SDBackendModule::TE),
tensor_storage_map,
true,
0,
false,
weight_manager,
sd_ctx_params->model_args);
result.diffusion = std::make_shared<PixArt::PixArtRunner>(ctx.backends.runtime_backend(SDBackendModule::DIFFUSION),
tensor_storage_map,
"model.diffusion_model",
weight_manager,
sd_ctx_params->model_args);
} else if (sd_version_is_mage_flow(version)) {
result.conditioner = std::make_shared<LLMEmbedder>(ctx.backends.runtime_backend(SDBackendModule::TE),
tensor_storage_map,
Expand Down Expand Up @@ -562,6 +577,23 @@ namespace sd::model_builders {
false,
vae_version,
weight_manager);
if (sd_version_is_pixart(version)) {
// Alpha-512 and Sigma share tensor layouts; Alpha-512 needs an explicit scale override.
if (tensor_storage_map.count("model.diffusion_model.adaln_single.emb.resolution_embedder.linear_1.weight") != 0) {
model->scale_factor = 0.18215f;
}
for (const auto& [key, value] : parse_key_value_args(sd_ctx_params->model_args, "model arg")) {
if (key == "pixart_vae_scale_factor") {
float parsed = 0.f;
if (parse_strict_float(value, parsed) && std::isfinite(parsed) && parsed > 0.f) {
model->scale_factor = parsed;
} else {
LOG_WARN("ignoring invalid PixArt model arg '%s=%s'", key.c_str(), value.c_str());
}
}
}
LOG_VERBOSE("pixart: VAE scale factor = %.5f", model->scale_factor);
}
if (sd_version_is_sdxl(version) &&
(strlen(SAFE_STR(sd_ctx_params->vae_path)) == 0 || sd_ctx_params->force_sdxl_vae_conv_scale || options.external_vae_is_invalid)) {
float vae_conv_2d_scale = 1.f / 32.f;
Expand Down
6 changes: 6 additions & 0 deletions src/tokenizers/t5_unigram_tokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ bool T5UniGramTokenizer::encode(const std::string& input, std::vector<int>& resu
std::vector<int32_t> tokens;
std::vector<std::string> token_strs;
std::string normalized = normalize(input);
if (normalized.empty()) {
// HF reference tokenizers emit no pieces for empty input; pad_tokens
// still appends EOS so the sequence becomes [EOS] + padding.
result = std::move(tokens);
return true;
}
auto splited_texts = split_with_special_tokens(normalized, special_tokens);
if (splited_texts.empty()) {
splited_texts.push_back(normalized); // for empty string
Expand Down
Loading