diff --git a/README.md b/README.md
index d99348cbf..a63dad9a8 100644
--- a/README.md
+++ b/README.md
@@ -61,12 +61,14 @@ API and command-line option may change frequently.***
- [SeFi-Image](./docs/sefi_image.md)
- [HiDream-O1-Image](./docs/hidream_o1_image.md)
- [Ideogram4](./docs/ideogram4.md)
+ - [LLaDA-Image](./docs/llada_image.md)
- [Image Edit Models](./docs/edit.md)
- [FLUX.1-Kontext-dev](./docs/kontext.md)
- [Qwen Image Edit series](./docs/qwen_image_edit.md)
- [LongCat Image Edit](./docs/longcat_image.md)
- [Boogu Image Edit](./docs/boogu_image.md)
- [Mage-Flow-Edit](./docs/mage_flow.md#image-editing)
+ - [LLaDA-Image Edit](./docs/llada_image.md#image-editing)
- Video Models
- [Wan2.1/Wan2.2](./docs/wan.md)
- [MiniMax-H3](./docs/minimax_h3.md)
diff --git a/assets/llada_image/edit_example.png b/assets/llada_image/edit_example.png
new file mode 100644
index 000000000..aaff84f41
Binary files /dev/null and b/assets/llada_image/edit_example.png differ
diff --git a/assets/llada_image/example.png b/assets/llada_image/example.png
new file mode 100644
index 000000000..63cc05f50
Binary files /dev/null and b/assets/llada_image/example.png differ
diff --git a/docs/edit.md b/docs/edit.md
index 9791d046d..9f8a9c3f3 100644
--- a/docs/edit.md
+++ b/docs/edit.md
@@ -17,6 +17,7 @@ Depending on the architecture, different models handle reference images differen
| [**Boogu Image Edit**](./boogu_image.md) | `z_image_omni` |
| **Krea2 (Community Edit LoRAs)** | `krea2_ostris_edit` |
| [**Mage-Flow-Edit**](./mage_flow.md#image-editing) | `mage_flow` |
+| [**LLaDA-Image**](./llada_image.md#image-editing) | `llada_image` |
| **Anima (Community Edit LoRAs)** | `cosmos_reference` |
Stable-diffusion.spp also supports basic Unet-based editing models like instruct-pix2pix or CosXL-Edit. This document is not about those.
diff --git a/docs/llada_image.md b/docs/llada_image.md
new file mode 100644
index 000000000..af865e503
--- /dev/null
+++ b/docs/llada_image.md
@@ -0,0 +1,153 @@
+# How to Use
+
+LLaDA-Image is a 6B text-to-image and instruction-guided editing model. The denoiser is a
+Lumina2/Z-Image-style NextDiT conditioned by a LLaDA2-MoE diffusion-LLM text encoder, and it
+reuses the Flux.2 VAE. Two checkpoints are published: a 50-step base model and
+LLaDA-Image-Turbo, a 4-step distilled model.
+
+## Download weights
+
+Four components are required: a transformer, a text encoder, a VAE, and a connectors file
+holding the QueryFormer, the text projection and, for editing, the SigVQ image encoder.
+
+The two published checkpoints are **not** interchangeable. LLaDA-Image-Turbo and LLaDA-Image
+ship different transformers, text encoders, QueryFormers and text projections; only the VAE,
+the SigVQ encoder and the tokenizer are shared. Mixing the two produces degraded output rather
+than a clean error, so keep each checkpoint's files together.
+
+Both need an external LLaDA2 `tokenizer.json`, which is not embedded in sd.cpp and is the same
+file for either checkpoint. Take `tokenizer/tokenizer.json` from either repository and pass it
+with `--tokenizer`. See [JSON tokenizers](tokenizers.md) for CLI and C API usage.
+
+### LLaDA-Image-Turbo (4 steps)
+
+Converted transformer, text encoder and pre-merged connectors are at
+https://huggingface.co/fszontagh/LLaDA-Image-Turbo-GGUF:
+
+- `llada-image-turbo-f16.gguf`
+- `llada-image-turbo-text_encoder-q8_0.gguf`
+- `llada-image-turbo-connectors.safetensors` for text to image, or
+ `llada-image-turbo-connectors-edit.safetensors`, which also carries the SigVQ encoder that
+ editing needs.
+
+Other quantizations of the transformer and the text encoder are in the same repository.
+
+The VAE comes from the original repository,
+https://huggingface.co/inclusionAI/LLaDA-Image-Turbo: `vae/diffusion_pytorch_model.safetensors`,
+referred to below as `llada_vae.safetensors`.
+
+### LLaDA-Image (50 steps)
+
+Converted transformer, text encoder and pre-merged connectors are at
+https://huggingface.co/fszontagh/LLaDA-Image-GGUF:
+
+- `llada-image-f16.gguf`
+- `llada-image-text_encoder-q8_0.gguf`
+- `llada-image-connectors.safetensors` for text to image, or
+ `llada-image-connectors-edit.safetensors`, which also carries the SigVQ encoder that editing
+ needs.
+
+Other quantizations of the transformer and the text encoder are in the same repository.
+
+The VAE comes from the original repository,
+https://huggingface.co/inclusionAI/LLaDA-Image, and is the same file as the Turbo one.
+
+### Converting the weights yourself
+
+The transformer has to go in through `--diffusion-model` so that its tensor names keep the
+prefix the loader expects, while the text encoder goes in through `-m`:
+
+```bash
+./bin/sd-cli -M convert --diffusion-model transformer/diffusion_pytorch_model.safetensors.index.json \
+ -o llada-image-f16.gguf --type f16
+./bin/sd-cli -M convert -m text_encoder/model.safetensors.index.json \
+ -o llada-image-text_encoder-q8_0.gguf --type q8_0
+```
+
+### Building the connector file yourself
+
+`--embeddings-connectors` takes one file, so the QueryFormer, the text projection and
+(for editing) the SigVQ encoder have to be combined into a single Safetensors file, each
+tensor name prefixed with its component name. Leaving `sigvq` out skips loading the 2.6 GB
+encoder:
+
+```python
+from safetensors.torch import load_file, save_file
+
+merged = {}
+for prefix, path in [
+ ("queryformer", "queryformer/diffusion_pytorch_model.safetensors"),
+ ("text_projection", "text_projection/diffusion_pytorch_model.safetensors"),
+ ("sigvq", "sigvq/diffusion_pytorch_model.safetensors"),
+]:
+ for name, tensor in load_file(path).items():
+ merged[f"{prefix}.{name}"] = tensor
+save_file(merged, "llada_connectors.safetensors")
+```
+
+## Examples
+
+### Text to image
+
+```bash
+./bin/sd-cli \
+ --diffusion-model /path/to/llada-image-turbo-f16.gguf \
+ --llm /path/to/llada-image-turbo-text_encoder-q8_0.gguf \
+ --tokenizer /path/to/tokenizer.json \
+ --vae /path/to/llada_vae.safetensors \
+ --embeddings-connectors /path/to/llada-image-turbo-connectors.safetensors \
+ --prompt "a lovely cat holding a sign says 'llada.cpp'" \
+ --width 1024 \
+ --height 1024 \
+ --steps 4 \
+ --cfg-scale 1.0 \
+ --seed 42 \
+ --output output.png
+```
+
+
+
+### Image editing
+
+```bash
+./bin/sd-cli \
+ --diffusion-model /path/to/llada-image-turbo-f16.gguf \
+ --llm /path/to/llada-image-turbo-text_encoder-q8_0.gguf \
+ --tokenizer /path/to/tokenizer.json \
+ --vae /path/to/llada_vae.safetensors \
+ --embeddings-connectors /path/to/llada-image-turbo-connectors-edit.safetensors \
+ --ref-image /path/to/input.png \
+ --prompt "change the sign text to 'sd.cpp'" \
+ --width 1024 \
+ --height 1024 \
+ --steps 4 \
+ --cfg-scale 1.0 \
+ --diffusion-fa \
+ --output output.png
+```
+
+
+
+See [edit.md](./edit.md) for the shared reference-image options. LLaDA-Image uses the
+`llada_image` preset by default.
+
+## Notes
+
+- Use 4 steps and `--cfg-scale 1.0` for LLaDA-Image-Turbo; the guidance is distilled away, so
+ a higher CFG degrades output and doubles the text encoder cost. The 50-step base model uses
+ `--steps 50 --cfg-scale 5`.
+- Width and height are rounded up to a multiple of 16. For editing the reference pipeline
+ requires them to be divisible by 32.
+- Edit the 50-step base model at 1024x1024. At 512x512 it returns the reference image almost
+ unchanged instead of applying the instruction; LLaDA-Image-Turbo edits correctly at both.
+- Editing runs the reference and the target in one sequence, so it needs roughly twice the
+ tokens of text to image at the same size. On 12 GB, editing at 1024x1024 needs
+ `--diffusion-fa`; without it the diffusion graph does not fit.
+- The weights total about 16 GB, but segmented execution streams them, so a much smaller
+ budget works. At 512x512, `--max-vram 6` costs almost nothing over unconstrained execution,
+ and `--max-vram 3` still produces byte-identical output at roughly 2.5x the time.
+- `--scheduler` defaults to `llada_image`, which reproduces the reference Kumaraswamy sigma
+ grid. `--extra-sample-args uniform=1` selects the uniform grid instead.
+- Prompt templating is handled automatically; pass a plain description.
+- VQ-conditioned generation (`generation_mode="vq"`, where the text encoder decodes image
+ tokens before diffusion) is not implemented.
diff --git a/examples/common/common.cpp b/examples/common/common.cpp
index e8ec40295..10884b4a5 100644
--- a/examples/common/common.cpp
+++ b/examples/common/common.cpp
@@ -1109,7 +1109,7 @@ ArgOptions SDGenerationParams::get_options() {
&hires_upscaler},
{"",
"--extra-sample-args",
- "extra sampler/scheduler/guidance args, key=value list. CFG supports guidance_schedule; APG supports apg_eta, apg_momentum, apg_norm_threshold, apg_norm_threshold_smoothing; SLG supports slg_uncond; lcm supports noise_clip_std, noise_scale_start, noise_scale_end; flux supports base_shift, max_shift; ltx2 supports max_shift, base_shift, stretch, terminal; euler_ge supports gamma; beta scheduler supports alpha, beta; logit_normal supports mu, std, logsnr_min, logsnr_max, resolution_aware; lms supports lms_max_order, lms_shift, lms_divisions; noise-injecting samplers support noise_sampler with value iid (default except for dpm++2m_sde_bt) or brownian_tree; brownian_tree_rng supports cpu (default), cuda, std_default or sampler_rng",
+ "extra sampler/scheduler/guidance args, key=value list. CFG supports guidance_schedule; APG supports apg_eta, apg_momentum, apg_norm_threshold, apg_norm_threshold_smoothing; SLG supports slg_uncond; lcm supports noise_clip_std, noise_scale_start, noise_scale_end; flux supports base_shift, max_shift; ltx2 supports max_shift, base_shift, stretch, terminal; euler_ge supports gamma; beta scheduler supports alpha, beta; logit_normal supports mu, std, logsnr_min, logsnr_max, resolution_aware; llada_image supports uniform; lms supports lms_max_order, lms_shift, lms_divisions; noise-injecting samplers support noise_sampler with value iid (default except for dpm++2m_sde_bt) or brownian_tree; brownian_tree_rng supports cpu (default), cuda, std_default or sampler_rng",
(int)',',
&extra_sample_args},
{"",
diff --git a/include/stable-diffusion.h b/include/stable-diffusion.h
index 9bbf8c757..28f33c1de 100644
--- a/include/stable-diffusion.h
+++ b/include/stable-diffusion.h
@@ -79,6 +79,7 @@ enum scheduler_t {
FLUX2_SCHEDULER,
FLUX_SCHEDULER,
BETA_SCHEDULER,
+ LLADA_IMAGE_SCHEDULER,
SCHEDULER_COUNT
};
diff --git a/src/conditioning/conditioner.hpp b/src/conditioning/conditioner.hpp
index 0566f93f9..a3bee137c 100644
--- a/src/conditioning/conditioner.hpp
+++ b/src/conditioning/conditioner.hpp
@@ -14,6 +14,7 @@
#include "core/util.h"
#include "model/diffusion/model.hpp"
#include "model/te/clip.hpp"
+#include "model/te/llada_image_te.h"
#include "model/te/llm.hpp"
#include "model/te/t5.hpp"
#include "model_loader.h"
@@ -3159,6 +3160,203 @@ struct LTXAVTextProjectionRunner : public GGMLRunner {
}
};
+// LLaDA-Image's text path is a three-stage pipeline rather than a single encoder pass:
+// the token embeddings feed a QueryFormer whose 256 queries are appended to the backbone
+// input, and the backbone's final hidden states are projected to the denoiser's caption dim.
+// Ref: LLaDAImagePipeline._encode_text.
+struct LLaDAImageEmbedder : public Conditioner {
+ std::shared_ptr tokenizer;
+ std::shared_ptr llm;
+ std::shared_ptr query_former;
+ std::shared_ptr text_projection;
+ std::shared_ptr sigvq;
+
+ std::string llm_prefix;
+ std::string query_former_prefix;
+ std::string text_projection_prefix;
+ std::string sigvq_prefix;
+
+ LLaDAImageEmbedder(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string& llm_prefix = "text_encoders.llm",
+ const std::string& query_former_prefix = "queryformer",
+ const std::string& text_projection_prefix = "text_projection",
+ const std::string& sigvq_prefix = "sigvq",
+ std::shared_ptr weight_manager = nullptr,
+ const TokenizerConfig& tokenizers = {})
+ : llm_prefix(llm_prefix),
+ query_former_prefix(query_former_prefix),
+ text_projection_prefix(text_projection_prefix),
+ sigvq_prefix(sigvq_prefix) {
+ if (!tokenizers.has(TokenizerConfig::MAIN)) {
+ throw std::runtime_error("LLaDA-Image requires an external LLaDA2 tokenizer.json; pass --tokenizer FILE or set sd_ctx_params_t::tokenizer");
+ }
+ llm = std::make_shared(LLM::LLMArch::LLADA2_MOE,
+ backend,
+ tensor_storage_map,
+ llm_prefix,
+ false,
+ weight_manager);
+ // <|endoftext|> doubles as the pad token in LLaDA2's tokenizer.json.
+ tokenizer = tokenizers.create(TokenizerConfig::MAIN, llm->config.vocab_size, 156892);
+ query_former = std::make_shared(backend,
+ tensor_storage_map,
+ query_former_prefix,
+ weight_manager);
+ text_projection = std::make_shared(backend,
+ tensor_storage_map,
+ text_projection_prefix,
+ weight_manager);
+
+ // SigVQ is only present when the user supplies the editing weights.
+ for (const auto& [name, _] : tensor_storage_map) {
+ if (starts_with(name, sigvq_prefix + ".")) {
+ sigvq = std::make_shared(backend,
+ tensor_storage_map,
+ sigvq_prefix,
+ weight_manager);
+ break;
+ }
+ }
+ }
+
+ void get_param_tensors(std::map& tensors) override {
+ llm->get_param_tensors(tensors, llm_prefix);
+ query_former->get_param_tensors(tensors, query_former_prefix);
+ text_projection->get_param_tensors(tensors, text_projection_prefix);
+ if (sigvq != nullptr) {
+ sigvq->get_param_tensors(tensors, sigvq_prefix);
+ }
+ }
+
+ void get_param_tensor_ops(std::map& tensor_ops) override {
+ llm->get_param_tensor_ops(tensor_ops);
+ }
+
+ void set_flash_attention_enabled(bool enabled) override {
+ llm->set_flash_attention_enabled(enabled);
+ query_former->set_flash_attention_enabled(enabled);
+ text_projection->set_flash_attention_enabled(enabled);
+ if (sigvq != nullptr) {
+ sigvq->set_flash_attention_enabled(enabled);
+ }
+ }
+
+ void set_max_graph_vram_bytes(size_t max_vram_bytes) override {
+ llm->set_max_graph_vram_bytes(max_vram_bytes);
+ query_former->set_max_graph_vram_bytes(max_vram_bytes);
+ text_projection->set_max_graph_vram_bytes(max_vram_bytes);
+ if (sigvq != nullptr) {
+ sigvq->set_max_graph_vram_bytes(max_vram_bytes);
+ }
+ }
+
+ void set_runtime_backends(const std::vector& backends) override {
+ llm->set_runtime_backends(backends);
+ }
+
+ void set_graph_cut_layer_split_enabled(bool enabled) override {
+ llm->set_graph_cut_layer_split_enabled(enabled);
+ }
+
+ void set_graph_cut_layer_split_backend_vram_limits(const std::vector& limits) override {
+ llm->set_graph_cut_layer_split_backend_vram_limits(limits);
+ }
+
+ void get_layer_split_param_tensors(std::map& tensors) override {
+ llm->get_param_tensors(tensors, llm_prefix);
+ }
+
+ void set_weight_adapter(const std::shared_ptr& adapter) override {
+ llm->set_weight_adapter(adapter);
+ query_former->set_weight_adapter(adapter);
+ text_projection->set_weight_adapter(adapter);
+ if (sigvq != nullptr) {
+ sigvq->set_weight_adapter(adapter);
+ }
+ }
+
+ void runner_end() override {
+ llm->runner_end();
+ query_former->runner_end();
+ text_projection->runner_end();
+ if (sigvq != nullptr) {
+ sigvq->runner_end();
+ }
+ }
+
+ SDCondition get_learned_condition(int n_threads,
+ const ConditionerParams& conditioner_params) override {
+ const int64_t num_queries = 256;
+
+ std::string text = conditioner_params.text;
+ while (!text.empty() && std::isspace(static_cast(text.front()))) {
+ text.erase(text.begin());
+ }
+ while (!text.empty() && std::isspace(static_cast(text.back()))) {
+ text.pop_back();
+ }
+ std::string prompt = text.empty()
+ ? "HUMAN Generate an image.\nASSISTANT\n"
+ : "HUMAN Generate an image: " + text + "\nASSISTANT\n";
+
+ std::vector tokens;
+ if (!tokenizer->encode(prompt, tokens, nullptr)) {
+ return {};
+ }
+ int64_t n_text = static_cast(tokens.size());
+ GGML_ASSERT(n_text > 0);
+
+ sd::Tensor text_ids({n_text}, std::vector(tokens.begin(), tokens.end()));
+ auto inputs_embeds = llm->compute_input_embeds(n_threads, text_ids);
+ auto query_embeds = query_former->compute(n_threads, inputs_embeds);
+
+ // splice_image_embeds() replaces tokens in place, so the query slots have to exist in
+ // input_ids; their ids are irrelevant because the embeddings are overwritten.
+ std::vector padded(tokens.begin(), tokens.end());
+ padded.resize(static_cast(n_text + num_queries), tokenizer->PAD_TOKEN_ID);
+ int64_t n_total = static_cast(padded.size());
+ sd::Tensor input_ids({n_total}, padded);
+
+ // Bidirectional everywhere except that the text tokens must not see the appended
+ // queries, matching backbone_attention_mask[:, :, :text_length, text_length:] = min.
+ const float mask_min = std::numeric_limits::lowest() / 4.0f;
+ sd::Tensor attention_mask({n_total, n_total});
+ for (int64_t i1 = 0; i1 < n_total; ++i1) {
+ for (int64_t i0 = 0; i0 < n_total; ++i0) {
+ float value = (i1 < n_text && i0 >= n_text) ? mask_min : 0.0f;
+ attention_mask[i0 + n_total * i1] = value;
+ }
+ }
+
+ LLM::ImageEmbeds image_embeds;
+ image_embeds.emplace_back(static_cast(n_text), query_embeds);
+
+ std::set out_layers = {static_cast(llm->config.num_layers) + 1};
+ auto hidden_states = llm->compute(n_threads,
+ input_ids,
+ attention_mask,
+ image_embeds,
+ out_layers);
+
+ SDCondition result;
+ result.c_crossattn = text_projection->compute(n_threads, hidden_states);
+
+ // Editing: SigVQ sees the reference at half the output resolution, as in
+ // LLaDAImagePipeline._encode_source_image.
+ if (sigvq != nullptr && conditioner_params.ref_images != nullptr && !conditioner_params.ref_images->empty()) {
+ const auto& ref = conditioner_params.ref_images->front();
+ auto resized = sd::ops::interpolate(ref,
+ {conditioner_params.width / 2,
+ conditioner_params.height / 2,
+ ref.shape()[2],
+ ref.shape()[3]});
+ result.extra_c_crossattns.push_back(sigvq->compute(n_threads, resized));
+ }
+ return result;
+ }
+};
+
struct LTXAVEmbedder : public Conditioner {
static constexpr int64_t kHiddenSize = 3840;
static constexpr int64_t kNumStates = 49;
diff --git a/src/model.h b/src/model.h
index 7a8bc757f..b419b0764 100644
--- a/src/model.h
+++ b/src/model.h
@@ -59,6 +59,7 @@ enum SDVersion {
VERSION_KREA2,
VERSION_MAGE_FLOW,
VERSION_SENSENOVA_U1_5,
+ VERSION_LLADA_IMAGE,
VERSION_ESRGAN,
VERSION_COUNT,
};
@@ -172,6 +173,13 @@ static inline bool sd_version_is_z_image(SDVersion version) {
return false;
}
+static inline bool sd_version_is_llada_image(SDVersion version) {
+ if (version == VERSION_LLADA_IMAGE) {
+ return true;
+ }
+ return false;
+}
+
static inline bool sd_version_is_boogu_image(SDVersion version) {
if (version == VERSION_BOOGU_IMAGE) {
return true;
@@ -251,7 +259,7 @@ static inline bool sd_version_uses_flux_vae(SDVersion version) {
}
static inline bool sd_version_uses_flux2_vae(SDVersion version) {
- if (sd_version_is_flux2(version) || sd_version_is_ernie_image(version) || sd_version_is_lens(version) || sd_version_is_ideogram4(version) || sd_version_is_sefi_image(version)) {
+ if (sd_version_is_flux2(version) || sd_version_is_ernie_image(version) || sd_version_is_lens(version) || sd_version_is_ideogram4(version) || sd_version_is_sefi_image(version) || sd_version_is_llada_image(version)) {
return true;
}
return false;
@@ -292,6 +300,7 @@ static inline bool sd_version_is_dit(SDVersion version) {
version == VERSION_HIDREAM_O1 ||
sd_version_is_anima(version) ||
sd_version_is_z_image(version) ||
+ sd_version_is_llada_image(version) ||
sd_version_is_boogu_image(version) ||
sd_version_is_ernie_image(version) ||
sd_version_is_lens(version) ||
diff --git a/src/model/common/ggml_block.hpp b/src/model/common/ggml_block.hpp
index 2aca72f4a..408c44a6a 100644
--- a/src/model/common/ggml_block.hpp
+++ b/src/model/common/ggml_block.hpp
@@ -835,21 +835,30 @@ class RMSNorm : public UnaryBlock {
protected:
int64_t hidden_size;
float eps;
+ bool elementwise_affine;
std::string prefix;
void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, std::string prefix = "") override {
- this->prefix = prefix;
+ this->prefix = prefix;
+ if (!elementwise_affine) {
+ return;
+ }
enum ggml_type wtype = GGML_TYPE_F32;
params["weight"] = ggml_new_tensor_1d(ctx, wtype, hidden_size);
}
public:
RMSNorm(int64_t hidden_size,
- float eps = 1e-06f)
+ float eps = 1e-06f,
+ bool elementwise_affine = true)
: hidden_size(hidden_size),
- eps(eps) {}
+ eps(eps),
+ elementwise_affine(elementwise_affine) {}
ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) override {
+ if (!elementwise_affine) {
+ return ggml_rms_norm(ctx->ggml_ctx, x, eps);
+ }
ggml_tensor* w = params["weight"];
if (ctx->weight_adapter) {
w = ctx->weight_adapter->patch_weight(ctx->ggml_ctx, ctx->backend, w, prefix + "weight");
diff --git a/src/model/common/rope.hpp b/src/model/common/rope.hpp
index 778bbf426..e44784936 100644
--- a/src/model/common/rope.hpp
+++ b/src/model/common/rope.hpp
@@ -929,6 +929,145 @@ namespace Rope {
return ids;
}
+ // LLaDA-Image shares Lumina2/z_image's axes layout, but assigns position (0,0,0) to the
+ // padding slots of the caption stream instead of continuing the caption ramp through them.
+ __STATIC_INLINE__ std::vector> gen_llada_image_ids(int h,
+ int w,
+ int patch_size,
+ int bs,
+ int context_len,
+ int seq_multi_of) {
+ int context_pad_len = bound_mod(context_len, seq_multi_of);
+ int padded_context_len = context_len + context_pad_len;
+ auto txt_ids = std::vector>(bs * padded_context_len, std::vector(3, 0.0f));
+ for (int i = 0; i < bs * padded_context_len; i++) {
+ int pos = i % padded_context_len;
+ if (pos < context_len) {
+ txt_ids[i][0] = pos + 1.f;
+ }
+ }
+
+ int axes_dim_num = 3;
+ int index = padded_context_len + 1;
+ auto img_ids = gen_flux_img_ids(h, w, patch_size, bs, axes_dim_num, index);
+
+ int img_pad_len = bound_mod(static_cast(img_ids.size() / bs), seq_multi_of);
+ if (img_pad_len > 0) {
+ std::vector> img_pad_ids(bs * img_pad_len, std::vector(3, 0.f));
+ img_ids = concat_ids(img_ids, img_pad_ids, bs);
+ }
+
+ return concat_ids(txt_ids, img_ids, bs);
+ }
+
+ // LLaDA-Image editing packs two caption copies (clean and noisy), the source and target
+ // latents anchored at their own caption's end position, and the SigVQ stream after both.
+ // Padding slots keep position (0,0,0), as in the text-only layout.
+ __STATIC_INLINE__ std::vector> gen_llada_image_edit_ids(int h,
+ int w,
+ int patch_size,
+ int context_len,
+ int sigvq_len,
+ int seq_multi_of) {
+ const int context_pad = bound_mod(context_len, seq_multi_of);
+ const int padded_context = context_len + context_pad;
+ const int h_len = (h + (patch_size / 2)) / patch_size;
+ const int w_len = (w + (patch_size / 2)) / patch_size;
+ const int image_len = h_len * w_len;
+ const int image_pad = bound_mod(image_len, seq_multi_of);
+ const int padded_image = image_len + image_pad;
+ const int sigvq_pad = bound_mod(sigvq_len, seq_multi_of);
+
+ std::vector> cap_ids;
+ std::vector cap_end_positions;
+ int cursor = 1;
+ for (int copy = 0; copy < 2; ++copy) {
+ for (int i = 0; i < padded_context; ++i) {
+ std::vector id(3, 0.f);
+ if (i < context_len) {
+ id[0] = static_cast(cursor + i);
+ }
+ cap_ids.push_back(id);
+ }
+ cursor += context_len;
+ cap_end_positions.push_back(cursor);
+ cursor += 2;
+ }
+
+ std::vector> img_ids;
+ for (int copy = 0; copy < 2; ++copy) {
+ auto ids = gen_flux_img_ids(h, w, patch_size, 1, 3, cap_end_positions[copy]);
+ img_ids.insert(img_ids.end(), ids.begin(), ids.end());
+ img_ids.insert(img_ids.end(), image_pad, std::vector(3, 0.f));
+ }
+
+ const int sigvq_start = static_cast(cap_ids.size() + img_ids.size()) + 1;
+ std::vector> sigvq_ids;
+ for (int i = 0; i < sigvq_len + sigvq_pad; ++i) {
+ std::vector id(3, 0.f);
+ if (i < sigvq_len) {
+ id[0] = static_cast(sigvq_start + i);
+ }
+ sigvq_ids.push_back(id);
+ }
+
+ std::vector> ids;
+ ids.reserve(cap_ids.size() + img_ids.size() + sigvq_ids.size());
+ ids.insert(ids.end(), cap_ids.begin(), cap_ids.end());
+ ids.insert(ids.end(), img_ids.begin(), img_ids.end());
+ ids.insert(ids.end(), sigvq_ids.begin(), sigvq_ids.end());
+ SD_UNUSED(padded_image);
+ return ids;
+ }
+
+ __STATIC_INLINE__ std::vector gen_llada_image_edit_pe(int h,
+ int w,
+ int patch_size,
+ int context_len,
+ int sigvq_len,
+ int seq_multi_of,
+ int theta,
+ const std::vector& axes_dim) {
+ auto ids = gen_llada_image_edit_ids(h, w, patch_size, context_len, sigvq_len, seq_multi_of);
+ return embed_nd(ids, 1, static_cast(theta), axes_dim, {});
+ }
+
+ __STATIC_INLINE__ std::vector gen_llada_image_pe(int h,
+ int w,
+ int patch_size,
+ int bs,
+ int context_len,
+ int seq_multi_of,
+ int theta,
+ bool circular_h,
+ bool circular_w,
+ const std::vector& axes_dim) {
+ std::vector> ids = gen_llada_image_ids(h, w, patch_size, bs, context_len, seq_multi_of);
+ std::vector> wrap_dims;
+ if ((circular_h || circular_w) && bs > 0 && axes_dim.size() >= 3) {
+ int pad_h = (patch_size - (h % patch_size)) % patch_size;
+ int pad_w = (patch_size - (w % patch_size)) % patch_size;
+ int h_len = (h + pad_h) / patch_size;
+ int w_len = (w + pad_w) / patch_size;
+ if (h_len > 0 && w_len > 0) {
+ size_t pos_len = ids.size() / bs;
+ wrap_dims.assign(axes_dim.size(), std::vector(pos_len, 0));
+ size_t cursor = context_len + bound_mod(context_len, seq_multi_of);
+ size_t img_tokens = static_cast(h_len) * static_cast(w_len);
+ for (size_t token_i = 0; token_i < img_tokens; ++token_i) {
+ if (circular_h) {
+ wrap_dims[1][cursor + token_i] = h_len;
+ }
+ if (circular_w) {
+ wrap_dims[2][cursor + token_i] = w_len;
+ }
+ }
+ }
+ }
+
+ return embed_nd(ids, bs, static_cast(theta), axes_dim, wrap_dims);
+ }
+
// Generate z_image positional embeddings
__STATIC_INLINE__ std::vector gen_z_image_pe(int h,
int w,
diff --git a/src/model/diffusion/llada_image.h b/src/model/diffusion/llada_image.h
new file mode 100644
index 000000000..3521d02f3
--- /dev/null
+++ b/src/model/diffusion/llada_image.h
@@ -0,0 +1,519 @@
+#ifndef __SD_MODEL_DIFFUSION_LLADA_IMAGE_H__
+#define __SD_MODEL_DIFFUSION_LLADA_IMAGE_H__
+
+#include
+#include
+
+#include "core/ggml_extend.h"
+#include "core/ggml_runner.h"
+#include "core/util.h"
+#include "model/common/ggml_block.hpp"
+#include "model/diffusion/model.hpp"
+#include "model/diffusion/z_image.hpp"
+#include "model_loader.h"
+
+// Ref: https://github.com/inclusionAI/LLaDA-Image/blob/main/src/models/transformer_llada_image.py
+//
+// The denoiser is Lumina2/z_image's NextDiT with identical hyperparameters, so the blocks are
+// reused from ZImage. Two things differ: every norm here is non-parametric (the checkpoint
+// carries no norm weights at all), and latents arrive already patchified from the Flux2 VAE,
+// so patch_size is 1 over 128 channels.
+
+namespace LLaDAImage {
+ constexpr int LLADA_IMAGE_GRAPH_SIZE = 20480;
+
+ struct LLaDAImageConfig {
+ int patch_size = 1;
+ int64_t hidden_size = 3840;
+ int64_t in_channels = 128;
+ int64_t out_channels = 128;
+ int64_t num_layers = 30;
+ int64_t num_refiner_layers = 2;
+ int64_t head_dim = 128;
+ int64_t num_heads = 30;
+ int64_t num_kv_heads = 30;
+ int64_t multiple_of = 256;
+ float ffn_dim_multiplier = 8.0f / 3.0f;
+ float norm_eps = 1e-5f;
+ bool qk_norm = true;
+ int64_t cap_feat_dim = 2560;
+ int64_t semantic_feat_dim = 4096;
+ int theta = 256;
+ std::vector axes_dim = {32, 48, 48};
+ int64_t axes_dim_sum = 128;
+
+ static int64_t count_blocks(const String2TensorStorage& tensor_storage_map,
+ const std::string& prefix,
+ const std::string& block_prefix) {
+ int64_t count = 0;
+ for (const auto& [name, _] : tensor_storage_map) {
+ if (!starts_with(name, prefix)) {
+ continue;
+ }
+ size_t pos = name.find(block_prefix);
+ if (pos == std::string::npos) {
+ continue;
+ }
+ auto items = split_string(name.substr(pos), '.');
+ if (items.size() > 1) {
+ count = std::max(count, atoi(items[1].c_str()) + 1);
+ }
+ }
+ return count;
+ }
+
+ static LLaDAImageConfig detect_from_weights(const String2TensorStorage& tensor_storage_map, const std::string& prefix) {
+ LLaDAImageConfig config;
+ int64_t detected_q_dim = 0;
+ int64_t detected_kv_dim = 0;
+
+ for (const auto& [name, tensor_storage] : tensor_storage_map) {
+ if (!starts_with(name, prefix)) {
+ continue;
+ }
+ if (ends_with(name, "x_embedder.weight") && tensor_storage.n_dims == 2) {
+ int64_t patch_area = config.patch_size * config.patch_size;
+ config.in_channels = tensor_storage.ne[0] / patch_area;
+ config.hidden_size = tensor_storage.ne[1];
+ } else if (ends_with(name, "cap_embedder.1.weight") && tensor_storage.n_dims == 2) {
+ config.cap_feat_dim = tensor_storage.ne[0];
+ config.hidden_size = tensor_storage.ne[1];
+ } else if (ends_with(name, "sigvq_embedder.1.weight") && tensor_storage.n_dims == 2) {
+ config.semantic_feat_dim = tensor_storage.ne[0];
+ } else if (ends_with(name, "layers.0.attention.to_q.weight") && tensor_storage.n_dims == 2) {
+ detected_q_dim = tensor_storage.ne[1];
+ } else if (ends_with(name, "layers.0.attention.to_k.weight") && tensor_storage.n_dims == 2) {
+ detected_kv_dim = tensor_storage.ne[1];
+ } else if (ends_with(name, "final_layer.linear.weight") && tensor_storage.n_dims == 2) {
+ int64_t patch_area = config.patch_size * config.patch_size;
+ config.out_channels = tensor_storage.ne[1] / patch_area;
+ }
+ }
+
+ int64_t detected_layers = count_blocks(tensor_storage_map, prefix, "layers.");
+ int64_t detected_refiner = std::max(count_blocks(tensor_storage_map, prefix, "noise_refiner."),
+ count_blocks(tensor_storage_map, prefix, "context_refiner."));
+ if (detected_layers > 0) {
+ config.num_layers = detected_layers;
+ }
+ if (detected_refiner > 0) {
+ config.num_refiner_layers = detected_refiner;
+ }
+ if (detected_q_dim > 0) {
+ config.num_heads = detected_q_dim / config.head_dim;
+ }
+ if (detected_kv_dim > 0) {
+ config.num_kv_heads = detected_kv_dim / config.head_dim;
+ } else if (detected_q_dim > 0) {
+ config.num_kv_heads = config.num_heads;
+ }
+
+ LOG_VERBOSE("llada_image: num_layers = %" PRId64 ", num_refiner_layers = %" PRId64 ", hidden_size = %" PRId64 ", num_heads = %" PRId64 ", num_kv_heads = %" PRId64 ", in_channels = %" PRId64 ", out_channels = %" PRId64 ", cap_feat_dim = %" PRId64 ", semantic_feat_dim = %" PRId64,
+ config.num_layers,
+ config.num_refiner_layers,
+ config.hidden_size,
+ config.num_heads,
+ config.num_kv_heads,
+ config.in_channels,
+ config.out_channels,
+ config.cap_feat_dim,
+ config.semantic_feat_dim);
+ return config;
+ }
+ };
+
+ class LLaDAImageModel : public GGMLBlock {
+ protected:
+ LLaDAImageConfig config;
+
+ void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "") override {
+ params["cap_pad_token"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, config.hidden_size);
+ params["x_pad_token"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, config.hidden_size);
+ params["sigvq_pad_token"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, config.hidden_size);
+ }
+
+ std::shared_ptr make_block(bool modulation) {
+ return std::make_shared(0,
+ config.hidden_size,
+ config.head_dim,
+ config.num_heads,
+ config.num_kv_heads,
+ config.multiple_of,
+ config.ffn_dim_multiplier,
+ config.norm_eps,
+ config.qk_norm,
+ modulation,
+ false,
+ true);
+ }
+
+ public:
+ LLaDAImageModel() = default;
+ LLaDAImageModel(LLaDAImageConfig config)
+ : config(config) {
+ blocks["x_embedder"] = std::make_shared(config.patch_size * config.patch_size * config.in_channels, config.hidden_size);
+ blocks["t_embedder"] = std::make_shared(MIN(config.hidden_size, 1024), 256, ZImage::ADALN_EMBED_DIM);
+
+ blocks["cap_embedder.0"] = std::make_shared(config.cap_feat_dim, config.norm_eps, false);
+ blocks["cap_embedder.1"] = std::make_shared(config.cap_feat_dim, config.hidden_size);
+
+ blocks["semantic_embedder.0"] = std::make_shared(config.semantic_feat_dim, config.norm_eps, false);
+ blocks["semantic_embedder.1"] = std::make_shared(config.semantic_feat_dim, config.hidden_size);
+ blocks["sigvq_embedder.0"] = std::make_shared(config.semantic_feat_dim, config.norm_eps, false);
+ blocks["sigvq_embedder.1"] = std::make_shared(config.semantic_feat_dim, config.hidden_size);
+
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ blocks["noise_refiner." + std::to_string(i)] = make_block(true);
+ blocks["context_refiner." + std::to_string(i)] = make_block(false);
+ blocks["sigvq_refiner." + std::to_string(i)] = make_block(false);
+ }
+ for (int i = 0; i < config.num_layers; i++) {
+ blocks["layers." + std::to_string(i)] = make_block(true);
+ }
+
+ blocks["final_layer"] = std::make_shared(config.hidden_size, config.patch_size, config.out_channels);
+ }
+
+ ggml_tensor* forward_core(GGMLRunnerContext* ctx,
+ ggml_tensor* x,
+ ggml_tensor* timestep,
+ ggml_tensor* context,
+ ggml_tensor* pe) {
+ auto x_embedder = std::dynamic_pointer_cast(blocks["x_embedder"]);
+ auto t_embedder = std::dynamic_pointer_cast(blocks["t_embedder"]);
+ auto cap_embedder_0 = std::dynamic_pointer_cast(blocks["cap_embedder.0"]);
+ auto cap_embedder_1 = std::dynamic_pointer_cast(blocks["cap_embedder.1"]);
+ auto final_layer = std::dynamic_pointer_cast(blocks["final_layer"]);
+
+ auto txt_pad_token = params["cap_pad_token"];
+ auto img_pad_token = params["x_pad_token"];
+
+ int64_t N = x->ne[2];
+ int64_t n_img_token = x->ne[1];
+ int64_t n_txt_token = context->ne[1];
+
+ // sdcpp's flow denoiser already hands over sigma * 1000, which is the range the
+ // reference reaches via its own t_scale, so no further scaling here.
+ auto t_emb = t_embedder->forward(ctx, timestep);
+
+ auto txt = cap_embedder_1->forward(ctx, cap_embedder_0->forward(ctx, context)); // [N, n_txt_token, hidden_size]
+ auto img = x_embedder->forward(ctx, x); // [N, n_img_token, hidden_size]
+ sd::ggml_graph_cut::mark_graph_cut(txt, "llada_image.prelude", "txt");
+ sd::ggml_graph_cut::mark_graph_cut(img, "llada_image.prelude", "img");
+ sd::ggml_graph_cut::mark_graph_cut(t_emb, "llada_image.prelude", "t_emb");
+
+ int64_t n_txt_pad_token = Rope::bound_mod(static_cast(n_txt_token), ZImage::SEQ_MULTI_OF);
+ if (n_txt_pad_token > 0) {
+ auto txt_pad_tokens = ggml_repeat_4d(ctx->ggml_ctx, txt_pad_token, txt_pad_token->ne[0], n_txt_pad_token, N, 1);
+ txt = ggml_concat(ctx->ggml_ctx, txt, txt_pad_tokens, 1);
+ }
+
+ int64_t n_img_pad_token = Rope::bound_mod(static_cast(n_img_token), ZImage::SEQ_MULTI_OF);
+ if (n_img_pad_token > 0) {
+ auto img_pad_tokens = ggml_repeat_4d(ctx->ggml_ctx, img_pad_token, img_pad_token->ne[0], n_img_pad_token, N, 1);
+ img = ggml_concat(ctx->ggml_ctx, img, img_pad_tokens, 1);
+ }
+
+ GGML_ASSERT(txt->ne[1] + img->ne[1] == pe->ne[3]);
+
+ auto txt_pe = ggml_ext_slice(ctx->ggml_ctx, pe, 3, 0, txt->ne[1]);
+ auto img_pe = ggml_ext_slice(ctx->ggml_ctx, pe, 3, txt->ne[1], pe->ne[3]);
+
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["context_refiner." + std::to_string(i)]);
+
+ txt = block->forward(ctx, txt, txt_pe, nullptr, nullptr);
+ sd::ggml_graph_cut::mark_graph_cut(txt, "llada_image.context_refiner." + std::to_string(i), "txt");
+ }
+
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["noise_refiner." + std::to_string(i)]);
+
+ img = block->forward(ctx, img, img_pe, nullptr, t_emb);
+ sd::ggml_graph_cut::mark_graph_cut(img, "llada_image.noise_refiner." + std::to_string(i), "img");
+ }
+
+ auto txt_img = ggml_concat(ctx->ggml_ctx, txt, img, 1);
+ sd::ggml_graph_cut::mark_graph_cut(txt_img, "llada_image.prelude", "txt_img");
+
+ for (int i = 0; i < config.num_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["layers." + std::to_string(i)]);
+
+ txt_img = block->forward(ctx, txt_img, pe, nullptr, t_emb);
+ sd::ggml_graph_cut::mark_graph_cut(txt_img, "llada_image.layers." + std::to_string(i), "txt_img");
+ }
+
+ txt_img = final_layer->forward(ctx, txt_img, t_emb);
+
+ return ggml_ext_slice(ctx->ggml_ctx, txt_img, 1, n_txt_token + n_txt_pad_token, n_txt_token + n_txt_pad_token + n_img_token);
+ }
+
+ ggml_tensor* pad_stream(GGMLRunnerContext* ctx, ggml_tensor* x, ggml_tensor* pad_token) {
+ int64_t n_pad = Rope::bound_mod(static_cast(x->ne[1]), ZImage::SEQ_MULTI_OF);
+ if (n_pad == 0) {
+ return x;
+ }
+ auto pads = ggml_repeat_4d(ctx->ggml_ctx, pad_token, pad_token->ne[0], n_pad, x->ne[2], 1);
+ return ggml_concat(ctx->ggml_ctx, x, pads, 1);
+ }
+
+ // Editing runs one joint sequence carrying two timesteps: the caption and source latent
+ // are clean (t = 0) while the second caption copy and the target latent are noisy. adaLN
+ // is a linear map of the timestep embedding, so feeding a per-token embedding selects the
+ // right modulation exactly, without duplicating the modulation projections.
+ ggml_tensor* forward_editing(GGMLRunnerContext* ctx,
+ ggml_tensor* x,
+ ggml_tensor* timestep,
+ ggml_tensor* context,
+ ggml_tensor* semantic,
+ ggml_tensor* source_latent,
+ ggml_tensor* pe) {
+ ggml_context* gctx = ctx->ggml_ctx;
+
+ auto x_embedder = std::dynamic_pointer_cast(blocks["x_embedder"]);
+ auto t_embedder = std::dynamic_pointer_cast(blocks["t_embedder"]);
+ auto cap_embedder_0 = std::dynamic_pointer_cast(blocks["cap_embedder.0"]);
+ auto cap_embedder_1 = std::dynamic_pointer_cast(blocks["cap_embedder.1"]);
+ auto sigvq_embed_0 = std::dynamic_pointer_cast(blocks["sigvq_embedder.0"]);
+ auto sigvq_embed_1 = std::dynamic_pointer_cast(blocks["sigvq_embedder.1"]);
+ auto final_layer = std::dynamic_pointer_cast(blocks["final_layer"]);
+
+ auto t_noisy = t_embedder->forward(ctx, timestep);
+ auto t_clean = t_embedder->forward(ctx, ggml_scale(gctx, timestep, 0.f));
+
+ auto per_token = [&](ggml_tensor* emb, int64_t n) {
+ return ggml_repeat_4d(gctx, emb, emb->ne[0], n, 1, 1);
+ };
+
+ auto cap = cap_embedder_1->forward(ctx, cap_embedder_0->forward(ctx, context));
+ cap = pad_stream(ctx, cap, params["cap_pad_token"]);
+ int64_t cap_len = cap->ne[1];
+ cap = ggml_concat(gctx, cap, cap, 1);
+
+ auto src = pad_stream(ctx, x_embedder->forward(ctx, source_latent), params["x_pad_token"]);
+ auto tgt_embed = x_embedder->forward(ctx, x);
+ int64_t n_img_token = tgt_embed->ne[1];
+ auto tgt = pad_stream(ctx, tgt_embed, params["x_pad_token"]);
+ int64_t img_len = tgt->ne[1];
+ auto img = ggml_concat(gctx, src, tgt, 1);
+
+ auto sig = sigvq_embed_1->forward(ctx, sigvq_embed_0->forward(ctx, semantic));
+ sig = pad_stream(ctx, sig, params["sigvq_pad_token"]);
+ int64_t sig_len = sig->ne[1];
+
+ GGML_ASSERT(cap_len * 2 + img_len * 2 + sig_len == pe->ne[3]);
+
+ auto cap_pe = ggml_ext_slice(gctx, pe, 3, 0, cap_len * 2);
+ auto img_pe = ggml_ext_slice(gctx, pe, 3, cap_len * 2, cap_len * 2 + img_len * 2);
+ auto sig_pe = ggml_ext_slice(gctx, pe, 3, cap_len * 2 + img_len * 2, pe->ne[3]);
+
+ auto img_adaln = ggml_concat(gctx, per_token(t_clean, img_len), per_token(t_noisy, img_len), 1);
+
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["context_refiner." + std::to_string(i)]);
+ cap = block->forward(ctx, cap, cap_pe, nullptr, nullptr);
+ }
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["noise_refiner." + std::to_string(i)]);
+ img = block->forward(ctx, img, img_pe, nullptr, img_adaln);
+ }
+ for (int i = 0; i < config.num_refiner_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["sigvq_refiner." + std::to_string(i)]);
+ sig = block->forward(ctx, sig, sig_pe, nullptr, nullptr);
+ }
+
+ auto seq = ggml_concat(gctx, ggml_concat(gctx, cap, img, 1), sig, 1);
+
+ auto cap_adaln = ggml_concat(gctx, per_token(t_clean, cap_len), per_token(t_noisy, cap_len), 1);
+ auto seq_adaln = ggml_concat(gctx,
+ ggml_concat(gctx, cap_adaln, img_adaln, 1),
+ per_token(t_clean, sig_len),
+ 1);
+
+ for (int i = 0; i < config.num_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["layers." + std::to_string(i)]);
+ seq = block->forward(ctx, seq, pe, nullptr, seq_adaln);
+ sd::ggml_graph_cut::mark_graph_cut(seq, "llada_image.layers." + std::to_string(i), "seq");
+ }
+
+ seq = final_layer->forward(ctx, seq, seq_adaln);
+
+ // Only the target latent is denoised; the source half of the image stream is context.
+ // The stream is padded to SEQ_MULTI_OF, so drop the pad tokens: they are not part of
+ // the latent grid that unpatchify reconstructs.
+ int64_t target_start = cap_len * 2 + img_len;
+ return ggml_ext_slice(gctx, seq, 1, target_start, target_start + n_img_token);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx,
+ ggml_tensor* x,
+ ggml_tensor* timestep,
+ ggml_tensor* context,
+ ggml_tensor* pe) {
+ // x: [N, C, H, W]
+ // timestep: [N,]
+ // context: [N, L, cap_feat_dim]
+ // pe: [L, d_head/2, 2, 2]
+ // return: [N, C, H, W]
+ int64_t W = x->ne[0];
+ int64_t H = x->ne[1];
+
+ int patch_size = config.patch_size;
+
+ auto img = DiT::pad_and_patchify(ctx, x, patch_size, patch_size, false);
+
+ auto out = forward_core(ctx, img, timestep, context, pe);
+
+ out = DiT::unpatchify_and_crop(ctx->ggml_ctx, out, H, W, patch_size, patch_size, false);
+
+ // The reference pipeline negates the model output before the scheduler step.
+ return ggml_ext_scale(ctx->ggml_ctx, out, -1.f);
+ }
+ };
+
+ struct LLaDAImageRunner : public DiffusionModelRunner {
+ public:
+ LLaDAImageConfig config;
+ LLaDAImageModel llada_image;
+ std::vector pe_vec;
+
+ LLaDAImageRunner(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "",
+ std::shared_ptr weight_manager = nullptr)
+ : DiffusionModelRunner(backend, prefix, weight_manager),
+ config(LLaDAImageConfig::detect_from_weights(tensor_storage_map, prefix)) {
+ llada_image = LLaDAImageModel(config);
+ llada_image.init(params_ctx, tensor_storage_map, prefix);
+ }
+
+ std::string get_desc() override {
+ return "llada_image";
+ }
+
+ void get_param_tensors(std::map& tensors, const std::string& prefix) override {
+ llada_image.get_param_tensors(tensors, prefix);
+ }
+
+ ggml_cgraph* build_graph(const sd::Tensor& x_tensor,
+ const sd::Tensor& timesteps_tensor,
+ const sd::Tensor& context_tensor) {
+ ggml_cgraph* gf = new_graph_custom(LLADA_IMAGE_GRAPH_SIZE);
+ ggml_tensor* x = make_input(x_tensor);
+ ggml_tensor* timesteps = make_input(timesteps_tensor);
+ GGML_ASSERT(x->ne[3] == 1);
+ GGML_ASSERT(!context_tensor.empty());
+ ggml_tensor* context = make_input(context_tensor);
+
+ pe_vec = Rope::gen_llada_image_pe(static_cast(x->ne[1]),
+ static_cast(x->ne[0]),
+ config.patch_size,
+ static_cast(x->ne[3]),
+ static_cast(context->ne[1]),
+ ZImage::SEQ_MULTI_OF,
+ config.theta,
+ circular_y_enabled,
+ circular_x_enabled,
+ config.axes_dim);
+ int pos_len = static_cast(pe_vec.size() / config.axes_dim_sum / 2);
+ auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, config.axes_dim_sum / 2, pos_len);
+ set_backend_tensor_data(pe, pe_vec.data());
+ auto runner_ctx = get_context();
+
+ ggml_tensor* out = llada_image.forward(&runner_ctx, x, timesteps, context, pe);
+
+ ggml_build_forward_expand(gf, out);
+
+ return gf;
+ }
+
+ sd::Tensor compute(int n_threads,
+ const sd::Tensor& x,
+ const sd::Tensor& timesteps,
+ const sd::Tensor& context) {
+ // x: [N, in_channels, h, w]
+ // timesteps: [N, ]
+ // context: [N, max_position, cap_feat_dim]
+ auto get_graph = [&]() -> ggml_cgraph* {
+ return build_graph(x, timesteps, context);
+ };
+
+ return restore_trailing_singleton_dims(GGMLRunner::compute(get_graph, n_threads, false), x.dim());
+ }
+
+ ggml_cgraph* build_edit_graph(const sd::Tensor& x_tensor,
+ const sd::Tensor& timesteps_tensor,
+ const sd::Tensor& context_tensor,
+ const sd::Tensor& semantic_tensor,
+ const sd::Tensor& source_tensor) {
+ ggml_cgraph* gf = new_graph_custom(LLADA_IMAGE_GRAPH_SIZE);
+ ggml_tensor* x = make_input(x_tensor);
+ ggml_tensor* timesteps = make_input(timesteps_tensor);
+ ggml_tensor* context = make_input(context_tensor);
+ ggml_tensor* semantic = make_input(semantic_tensor);
+ ggml_tensor* source = make_input(source_tensor);
+ GGML_ASSERT(x->ne[3] == 1);
+
+ pe_vec = Rope::gen_llada_image_edit_pe(static_cast(x->ne[1]),
+ static_cast(x->ne[0]),
+ config.patch_size,
+ static_cast(context->ne[1]),
+ static_cast(semantic->ne[1]),
+ ZImage::SEQ_MULTI_OF,
+ config.theta,
+ config.axes_dim);
+ int pos_len = static_cast(pe_vec.size() / config.axes_dim_sum / 2);
+ auto pe = ggml_new_tensor_4d(compute_ctx, GGML_TYPE_F32, 2, 2, config.axes_dim_sum / 2, pos_len);
+ set_backend_tensor_data(pe, pe_vec.data());
+ auto runner_ctx = get_context();
+
+ int64_t W = x->ne[0];
+ int64_t H = x->ne[1];
+ auto target = DiT::pad_and_patchify(&runner_ctx, x, config.patch_size, config.patch_size, false);
+ auto src = DiT::pad_and_patchify(&runner_ctx, source, config.patch_size, config.patch_size, false);
+
+ auto out = llada_image.forward_editing(&runner_ctx, target, timesteps, context, semantic, src, pe);
+ out = DiT::unpatchify_and_crop(runner_ctx.ggml_ctx, out, H, W, config.patch_size, config.patch_size, false);
+ out = ggml_ext_scale(runner_ctx.ggml_ctx, out, -1.f);
+
+ ggml_build_forward_expand(gf, out);
+ return gf;
+ }
+
+ sd::Tensor compute(int n_threads,
+ const DiffusionParams& diffusion_params) override {
+ GGML_ASSERT(diffusion_params.x != nullptr);
+ GGML_ASSERT(diffusion_params.timesteps != nullptr);
+
+ const auto* extra = std::get_if(&diffusion_params.extra);
+ bool has_semantic = extra != nullptr && extra->semantic != nullptr && !extra->semantic->empty();
+ bool has_ref_latent = diffusion_params.ref_latents != nullptr && !diffusion_params.ref_latents->empty();
+ if (has_semantic != has_ref_latent) {
+ LOG_WARN(
+ "llada_image: editing needs both the SigVQ features and the reference latent "
+ "(have semantic: %d, reference latent: %d); falling back to text to image",
+ static_cast(has_semantic),
+ static_cast(has_ref_latent));
+ }
+ if (has_semantic && has_ref_latent) {
+ auto get_graph = [&]() -> ggml_cgraph* {
+ return build_edit_graph(*diffusion_params.x,
+ *diffusion_params.timesteps,
+ tensor_or_empty(diffusion_params.context),
+ *extra->semantic,
+ diffusion_params.ref_latents->front());
+ };
+ return restore_trailing_singleton_dims(GGMLRunner::compute(get_graph, n_threads, false),
+ diffusion_params.x->dim());
+ }
+
+ return compute(n_threads,
+ *diffusion_params.x,
+ *diffusion_params.timesteps,
+ tensor_or_empty(diffusion_params.context));
+ }
+ };
+
+} // namespace LLaDAImage
+
+#endif // __SD_MODEL_DIFFUSION_LLADA_IMAGE_H__
diff --git a/src/model/diffusion/model.hpp b/src/model/diffusion/model.hpp
index a4b3c38fd..b6334e8e6 100644
--- a/src/model/diffusion/model.hpp
+++ b/src/model/diffusion/model.hpp
@@ -39,6 +39,9 @@ const std::unordered_map REF_IMAGE_PRESETS = {
{"z_image_omni", {true, true, Rope::RefIndexMode::FIXED, false, true, -1, RefImageResizeMode::AREA, -1, -1}},
{"krea2_ostris_edit", {true, true, Rope::RefIndexMode::INCREASE, true, true, -1, RefImageResizeMode::AREA, -1, -1}},
{"krea2_edit", {true, true, Rope::RefIndexMode::INCREASE, false, true, -1, RefImageResizeMode::LONGEST_SIDE, 768, 768}},
+ // pass_to_vlm routes the reference image to the conditioner, which is where LLaDA-Image's
+ // SigVQ encoder lives; it does its own half-resolution resize.
+ {"llada_image", {true, true, Rope::RefIndexMode::FIXED, true, true, -1, RefImageResizeMode::NONE, -1, -1}},
{"cosmos_reference", {false, true, Rope::RefIndexMode::INCREASE, false, false, -1, RefImageResizeMode::NONE, -1, -1}},
};
@@ -127,6 +130,11 @@ struct HunyuanVideoDiffusionExtra {
const sd::Tensor* timestep_r = nullptr;
};
+struct LLaDAImageDiffusionExtra {
+ // SigVQ semantic features of the reference image; present only in editing mode.
+ const sd::Tensor* semantic = nullptr;
+};
+
using DiffusionExtraParams = std::variant;
+ HunyuanVideoDiffusionExtra,
+ LLaDAImageDiffusionExtra>;
struct DiffusionParams {
const sd::Tensor* x = nullptr;
diff --git a/src/model/diffusion/z_image.hpp b/src/model/diffusion/z_image.hpp
index 4ae47e268..a1012cad8 100644
--- a/src/model/diffusion/z_image.hpp
+++ b/src/model/diffusion/z_image.hpp
@@ -131,16 +131,30 @@ namespace ZImage {
int64_t num_heads;
int64_t num_kv_heads;
bool qk_norm;
+ bool split_qkv;
public:
- JointAttention(int64_t hidden_size, int64_t head_dim, int64_t num_heads, int64_t num_kv_heads, bool qk_norm)
- : head_dim(head_dim), num_heads(num_heads), num_kv_heads(num_kv_heads), qk_norm(qk_norm) {
- blocks["qkv"] = std::make_shared(hidden_size, (num_heads + num_kv_heads * 2) * head_dim, false);
- float scale = 1.f;
- blocks["out"] = std::make_shared(num_heads * head_dim, hidden_size, false, false, false, scale);
+ JointAttention(int64_t hidden_size,
+ int64_t head_dim,
+ int64_t num_heads,
+ int64_t num_kv_heads,
+ bool qk_norm,
+ bool norm_elementwise_affine = true,
+ bool split_qkv = false)
+ : head_dim(head_dim), num_heads(num_heads), num_kv_heads(num_kv_heads), qk_norm(qk_norm), split_qkv(split_qkv) {
+ float scale = 1.f;
+ if (split_qkv) {
+ blocks["to_q"] = std::make_shared(hidden_size, num_heads * head_dim, false);
+ blocks["to_k"] = std::make_shared(hidden_size, num_kv_heads * head_dim, false);
+ blocks["to_v"] = std::make_shared(hidden_size, num_kv_heads * head_dim, false);
+ blocks["to_out.0"] = std::make_shared(num_heads * head_dim, hidden_size, false, false, false, scale);
+ } else {
+ blocks["qkv"] = std::make_shared(hidden_size, (num_heads + num_kv_heads * 2) * head_dim, false);
+ blocks["out"] = std::make_shared(num_heads * head_dim, hidden_size, false, false, false, scale);
+ }
if (qk_norm) {
- blocks["q_norm"] = std::make_shared(head_dim);
- blocks["k_norm"] = std::make_shared(head_dim);
+ blocks["q_norm"] = std::make_shared(head_dim, 1e-06f, norm_elementwise_affine);
+ blocks["k_norm"] = std::make_shared(head_dim, 1e-06f, norm_elementwise_affine);
}
}
@@ -151,8 +165,35 @@ namespace ZImage {
// x: [N, n_token, hidden_size]
int64_t n_token = x->ne[1];
int64_t N = x->ne[2];
- auto qkv_proj = std::dynamic_pointer_cast(blocks["qkv"]);
- auto out_proj = std::dynamic_pointer_cast(blocks["out"]);
+ auto out_proj = std::dynamic_pointer_cast(blocks[split_qkv ? "to_out.0" : "out"]);
+
+ if (split_qkv) {
+ auto q_proj = std::dynamic_pointer_cast(blocks["to_q"]);
+ auto k_proj = std::dynamic_pointer_cast(blocks["to_k"]);
+ auto v_proj = std::dynamic_pointer_cast(blocks["to_v"]);
+
+ if (sd_backend_is(ctx->backend, "ROCm")) {
+ out_proj->set_scale(1.f / 16.f);
+ out_proj->set_force_prec_f32(true);
+ q_proj->set_force_prec_f32(true);
+ k_proj->set_force_prec_f32(true);
+ v_proj->set_force_prec_f32(true);
+ }
+
+ auto q = ggml_reshape_4d(ctx->ggml_ctx, q_proj->forward(ctx, x), head_dim, num_heads, n_token, N);
+ auto k = ggml_reshape_4d(ctx->ggml_ctx, k_proj->forward(ctx, x), head_dim, num_kv_heads, n_token, N);
+ auto v = ggml_reshape_4d(ctx->ggml_ctx, v_proj->forward(ctx, x), head_dim, num_kv_heads, n_token, N);
+
+ if (qk_norm) {
+ q = std::dynamic_pointer_cast(blocks["q_norm"])->forward(ctx, q);
+ k = std::dynamic_pointer_cast(blocks["k_norm"])->forward(ctx, k);
+ }
+
+ auto out = Rope::attention(ctx, q, k, v, pe, mask, 1.f / 128.f);
+ return out_proj->forward(ctx, out);
+ }
+
+ auto qkv_proj = std::dynamic_pointer_cast(blocks["qkv"]);
if (sd_backend_is(ctx->backend, "ROCm")) {
out_proj->set_scale(1.f / 16.f);
@@ -252,9 +293,12 @@ namespace ZImage {
ggml_tensor* x,
ggml_tensor* scale) {
// x: [N, L, C]
- // scale: [N, C]
- scale = ggml_reshape_3d(ctx, scale, scale->ne[0], 1, scale->ne[1]); // [N, 1, C]
- x = ggml_add(ctx, x, ggml_mul(ctx, x, scale));
+ // scale: [N, C], or [N, L, C] when the caller modulates per token (LLaDA-Image editing
+ // feeds a per-token timestep embedding so each segment carries its own modulation).
+ if (scale->ne[1] != x->ne[1]) {
+ scale = ggml_reshape_3d(ctx, scale, scale->ne[0], 1, scale->ne[1]); // [N, 1, C]
+ }
+ x = ggml_add(ctx, x, ggml_mul(ctx, x, scale));
return x;
}
@@ -272,14 +316,16 @@ namespace ZImage {
float ffn_dim_multiplier,
float norm_eps,
bool qk_norm,
- bool modulation = true)
+ bool modulation = true,
+ bool norm_elementwise_affine = true,
+ bool split_qkv = false)
: modulation(modulation) {
- blocks["attention"] = std::make_shared(hidden_size, head_dim, num_heads, num_kv_heads, qk_norm);
+ blocks["attention"] = std::make_shared(hidden_size, head_dim, num_heads, num_kv_heads, qk_norm, norm_elementwise_affine, split_qkv);
blocks["feed_forward"] = std::make_shared(hidden_size, hidden_size, multiple_of, ffn_dim_multiplier);
- blocks["attention_norm1"] = std::make_shared(hidden_size, norm_eps);
- blocks["ffn_norm1"] = std::make_shared(hidden_size, norm_eps);
- blocks["attention_norm2"] = std::make_shared(hidden_size, norm_eps);
- blocks["ffn_norm2"] = std::make_shared(hidden_size, norm_eps);
+ blocks["attention_norm1"] = std::make_shared(hidden_size, norm_eps, norm_elementwise_affine);
+ blocks["ffn_norm1"] = std::make_shared(hidden_size, norm_eps, norm_elementwise_affine);
+ blocks["attention_norm2"] = std::make_shared(hidden_size, norm_eps, norm_elementwise_affine);
+ blocks["ffn_norm2"] = std::make_shared(hidden_size, norm_eps, norm_elementwise_affine);
if (modulation) {
blocks["adaLN_modulation.0"] = std::make_shared(MIN(hidden_size, ADALN_EMBED_DIM), 4 * hidden_size);
}
diff --git a/src/model/te/llada_image_te.h b/src/model/te/llada_image_te.h
new file mode 100644
index 000000000..e7492832c
--- /dev/null
+++ b/src/model/te/llada_image_te.h
@@ -0,0 +1,604 @@
+#ifndef __SD_MODEL_TE_LLADA_IMAGE_TE_H__
+#define __SD_MODEL_TE_LLADA_IMAGE_TE_H__
+
+#include
+#include
+#include
+
+#include "core/ggml_extend.h"
+#include "core/ggml_runner.h"
+#include "model/common/ggml_block.hpp"
+#include "model_loader.h"
+
+// The conditioning components LLaDA-Image puts around its LLaDA2-MoE backbone.
+// Ref: LLaDAImageQueryFormerModel / LLaDAImageTextProjectionModel in
+// https://github.com/inclusionAI/LLaDA-Image/blob/main/src/models/transformer_llada_image.py
+//
+// QueryFormer turns the LLaDA token embeddings into 256 learned queries that the pipeline
+// appends to the backbone input; TextProjection maps the backbone hidden states to the
+// denoiser's caption dimension. Neither uses RoPE, and every norm is parameter-free.
+// Both MLPs use the tanh GELU approximation, so ggml_gelu (not ggml_gelu_erf).
+//
+// SigVQ is the editing-only image encoder: a 40-layer ViT whose output is quantized against a
+// 16384-entry codebook, with the resulting ids embedded and projected into the semantic features
+// the denoiser consumes. Its MLP uses the exact erf GELU, unlike the two above.
+
+namespace LLaDAImageTE {
+ constexpr int LLADA_IMAGE_TE_GRAPH_SIZE = 16384;
+
+ struct QueryFormerConfig {
+ int64_t num_queries = 256;
+ int64_t hidden_size = 2048;
+ int64_t num_layers = 1;
+ int64_t num_heads = 16;
+ int64_t intermediate_size = 8192;
+ float norm_eps = 1e-6f;
+ };
+
+ struct TextProjectionConfig {
+ int64_t hidden_size = 2048;
+ int64_t intermediate_size = 8960;
+ int64_t num_layers = 6;
+ int64_t num_heads = 32;
+ int64_t projection_dim = 2560;
+ float norm_eps = 1e-6f;
+ };
+
+ // Cross-attention with a single fused in_proj over q (from the queries) and k/v (from the
+ // token embeddings). The checkpoint stores in_proj as one [3*hidden, hidden] parameter.
+ struct QueryAttention : public GGMLBlock {
+ protected:
+ int64_t hidden_size;
+ int64_t num_heads;
+
+ void init_params(ggml_context* ctx,
+ const String2TensorStorage& tensor_storage_map = {},
+ std::string prefix = "") override {
+ GGMLBlock::init_params(ctx, tensor_storage_map, prefix);
+ enum ggml_type wtype = get_type(prefix + "in_proj_weight", tensor_storage_map, GGML_TYPE_F32);
+ params["in_proj_weight"] = ggml_new_tensor_2d(ctx, wtype, hidden_size, hidden_size * 3);
+ params["in_proj_bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, hidden_size * 3);
+ }
+
+ public:
+ QueryAttention(int64_t hidden_size, int64_t num_heads)
+ : hidden_size(hidden_size), num_heads(num_heads) {
+ blocks["out_proj"] = std::make_shared(hidden_size, hidden_size, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx,
+ ggml_tensor* query,
+ ggml_tensor* context,
+ ggml_tensor* mask = nullptr) {
+ // query: [N, num_queries, hidden_size], context: [N, n_token, hidden_size]
+ ggml_context* gctx = ctx->ggml_ctx;
+ auto out_proj = std::dynamic_pointer_cast(blocks["out_proj"]);
+
+ auto w = params["in_proj_weight"];
+ auto b = params["in_proj_bias"];
+
+ auto slice_w = [&](int64_t index) {
+ return ggml_ext_slice(gctx, w, 1, index * hidden_size, (index + 1) * hidden_size);
+ };
+ auto slice_b = [&](int64_t index) {
+ return ggml_ext_slice(gctx, b, 0, index * hidden_size, (index + 1) * hidden_size);
+ };
+
+ auto q = ggml_ext_linear(gctx, query, slice_w(0), slice_b(0));
+ auto k = ggml_ext_linear(gctx, context, slice_w(1), slice_b(1));
+ auto v = ggml_ext_linear(gctx, context, slice_w(2), slice_b(2));
+
+ auto x = ggml_ext_attention_ext(ctx, q, k, v, num_heads, mask); // [N, num_queries, hidden_size]
+ return out_proj->forward(ctx, x);
+ }
+ };
+
+ struct QueryFormerBlock : public GGMLBlock {
+ protected:
+ QueryFormerConfig config;
+
+ public:
+ QueryFormerBlock(const QueryFormerConfig& config)
+ : config(config) {
+ blocks["norm_q"] = std::make_shared(config.hidden_size, config.norm_eps, false);
+ blocks["norm_k"] = std::make_shared(config.hidden_size, config.norm_eps, false);
+ blocks["cross_attn"] = std::make_shared(config.hidden_size, config.num_heads);
+ blocks["norm1"] = std::make_shared(config.hidden_size, config.norm_eps, false);
+ blocks["mlp.fc1"] = std::make_shared(config.hidden_size, config.intermediate_size, true);
+ blocks["mlp.fc2"] = std::make_shared(config.intermediate_size, config.hidden_size, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx,
+ ggml_tensor* query,
+ ggml_tensor* context,
+ ggml_tensor* mask = nullptr) {
+ auto norm_q = std::dynamic_pointer_cast(blocks["norm_q"]);
+ auto norm_k = std::dynamic_pointer_cast(blocks["norm_k"]);
+ auto cross_attn = std::dynamic_pointer_cast(blocks["cross_attn"]);
+ auto norm1 = std::dynamic_pointer_cast(blocks["norm1"]);
+ auto fc1 = std::dynamic_pointer_cast(blocks["mlp.fc1"]);
+ auto fc2 = std::dynamic_pointer_cast(blocks["mlp.fc2"]);
+
+ // The reference overwrites query_embeds with its normalized value before the
+ // residual add, so both residuals here are on normalized activations.
+ query = norm_q->forward(ctx, query);
+ auto ctx_n = norm_k->forward(ctx, context);
+ query = ggml_add(ctx->ggml_ctx, query, cross_attn->forward(ctx, query, ctx_n, mask));
+ query = norm1->forward(ctx, query);
+
+ auto h = fc1->forward(ctx, query);
+ h = ggml_gelu(ctx->ggml_ctx, h);
+ h = fc2->forward(ctx, h);
+ return ggml_add(ctx->ggml_ctx, query, h);
+ }
+ };
+
+ struct QueryFormerModel : public GGMLBlock {
+ protected:
+ QueryFormerConfig config;
+
+ void init_params(ggml_context* ctx,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "") override {
+ params["meta_queries"] = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, config.hidden_size, config.num_queries);
+ }
+
+ public:
+ QueryFormerModel() = default;
+ QueryFormerModel(const QueryFormerConfig& config)
+ : config(config) {
+ for (int i = 0; i < config.num_layers; i++) {
+ blocks["query_blocks." + std::to_string(i)] = std::make_shared(config);
+ }
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx,
+ ggml_tensor* inputs_embeds,
+ ggml_tensor* mask = nullptr) {
+ // inputs_embeds: [N, n_token, hidden_size] -> [N, num_queries, hidden_size]
+ auto query = params["meta_queries"];
+ query = ggml_reshape_3d(ctx->ggml_ctx, query, config.hidden_size, config.num_queries, 1);
+
+ for (int i = 0; i < config.num_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["query_blocks." + std::to_string(i)]);
+ query = block->forward(ctx, query, inputs_embeds, mask);
+ }
+ return query;
+ }
+ };
+
+ struct TextProjectionAttention : public GGMLBlock {
+ protected:
+ int64_t num_heads;
+ int64_t head_dim;
+
+ public:
+ TextProjectionAttention(const TextProjectionConfig& config)
+ : num_heads(config.num_heads), head_dim(config.hidden_size / config.num_heads) {
+ blocks["q_proj"] = std::make_shared(config.hidden_size, config.hidden_size, true);
+ blocks["k_proj"] = std::make_shared(config.hidden_size, config.hidden_size, true);
+ blocks["v_proj"] = std::make_shared(config.hidden_size, config.hidden_size, true);
+ blocks["out_proj"] = std::make_shared(config.hidden_size, config.hidden_size, true);
+ blocks["q_norm"] = std::make_shared(head_dim, config.norm_eps, false);
+ blocks["k_norm"] = std::make_shared(head_dim, config.norm_eps, false);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [N, n_token, hidden_size]
+ ggml_context* gctx = ctx->ggml_ctx;
+ int64_t n_token = x->ne[1];
+ int64_t N = x->ne[2];
+
+ auto q_proj = std::dynamic_pointer_cast(blocks["q_proj"]);
+ auto k_proj = std::dynamic_pointer_cast(blocks["k_proj"]);
+ auto v_proj = std::dynamic_pointer_cast(blocks["v_proj"]);
+ auto out_proj = std::dynamic_pointer_cast(blocks["out_proj"]);
+ auto q_norm = std::dynamic_pointer_cast(blocks["q_norm"]);
+ auto k_norm = std::dynamic_pointer_cast(blocks["k_norm"]);
+
+ auto q = q_proj->forward(ctx, x);
+ auto k = k_proj->forward(ctx, x);
+ auto v = v_proj->forward(ctx, x);
+
+ q = ggml_reshape_4d(gctx, q, head_dim, num_heads, n_token, N);
+ k = ggml_reshape_4d(gctx, k, head_dim, num_heads, n_token, N);
+ q = q_norm->forward(ctx, q);
+ k = k_norm->forward(ctx, k);
+ q = ggml_reshape_3d(gctx, q, head_dim * num_heads, n_token, N);
+ k = ggml_reshape_3d(gctx, k, head_dim * num_heads, n_token, N);
+
+ auto out = ggml_ext_attention_ext(ctx, q, k, v, num_heads);
+ return out_proj->forward(ctx, out);
+ }
+ };
+
+ struct TextProjectionBlock : public GGMLBlock {
+ public:
+ TextProjectionBlock(const TextProjectionConfig& config) {
+ blocks["self_attn"] = std::make_shared(config);
+ blocks["layer_norm1"] = std::make_shared(config.hidden_size, config.norm_eps, false);
+ blocks["layer_norm2"] = std::make_shared(config.hidden_size, config.norm_eps, false);
+ blocks["mlp.fc1"] = std::make_shared(config.hidden_size, config.intermediate_size, true);
+ blocks["mlp.fc2"] = std::make_shared(config.intermediate_size, config.hidden_size, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto self_attn = std::dynamic_pointer_cast(blocks["self_attn"]);
+ auto layer_norm1 = std::dynamic_pointer_cast(blocks["layer_norm1"]);
+ auto layer_norm2 = std::dynamic_pointer_cast(blocks["layer_norm2"]);
+ auto fc1 = std::dynamic_pointer_cast(blocks["mlp.fc1"]);
+ auto fc2 = std::dynamic_pointer_cast(blocks["mlp.fc2"]);
+
+ x = ggml_add(ctx->ggml_ctx, x, self_attn->forward(ctx, layer_norm1->forward(ctx, x)));
+
+ auto h = fc1->forward(ctx, layer_norm2->forward(ctx, x));
+ h = ggml_gelu(ctx->ggml_ctx, h);
+ h = fc2->forward(ctx, h);
+ return ggml_add(ctx->ggml_ctx, x, h);
+ }
+ };
+
+ struct TextProjectionModel : public GGMLBlock {
+ protected:
+ TextProjectionConfig config;
+
+ public:
+ TextProjectionModel() = default;
+ TextProjectionModel(const TextProjectionConfig& config)
+ : config(config) {
+ for (int i = 0; i < config.num_layers; i++) {
+ blocks["layers." + std::to_string(i)] = std::make_shared(config);
+ }
+ blocks["projector"] = std::make_shared(config.hidden_size, config.projection_dim, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [N, n_token, hidden_size] -> [N, n_token, projection_dim]
+ for (int i = 0; i < config.num_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["layers." + std::to_string(i)]);
+ x = block->forward(ctx, x);
+ }
+ auto projector = std::dynamic_pointer_cast(blocks["projector"]);
+ return projector->forward(ctx, x);
+ }
+ };
+
+ struct SigVQConfig {
+ int64_t image_size = 2048;
+ int64_t patch_size = 16;
+ int64_t in_channels = 3;
+ int64_t hidden_size = 1536;
+ int64_t intermediate_size = 6144;
+ int64_t num_layers = 40;
+ int64_t num_heads = 16;
+ int64_t codebook_size = 16384;
+ int64_t codebook_embed_dim = 2048;
+ int64_t semantic_embed_dim = 4096;
+ float norm_eps = 1e-6f;
+ };
+
+ struct SigVQAttention : public GGMLBlock {
+ protected:
+ int64_t num_heads;
+ int64_t head_dim;
+
+ public:
+ SigVQAttention(const SigVQConfig& config)
+ : num_heads(config.num_heads), head_dim(config.hidden_size / config.num_heads) {
+ blocks["qkv"] = std::make_shared(config.hidden_size, config.hidden_size * 3, true);
+ blocks["proj"] = std::make_shared(config.hidden_size, config.hidden_size, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ // x: [N, n_token, hidden_size]
+ ggml_context* gctx = ctx->ggml_ctx;
+ auto qkv_proj = std::dynamic_pointer_cast(blocks["qkv"]);
+ auto out_proj = std::dynamic_pointer_cast(blocks["proj"]);
+
+ int64_t hidden_size = num_heads * head_dim;
+ auto qkv = qkv_proj->forward(ctx, x);
+ auto q = ggml_ext_slice(gctx, qkv, 0, 0, hidden_size);
+ auto k = ggml_ext_slice(gctx, qkv, 0, hidden_size, hidden_size * 2);
+ auto v = ggml_ext_slice(gctx, qkv, 0, hidden_size * 2, hidden_size * 3);
+
+ auto out = ggml_ext_attention_ext(ctx, q, k, v, num_heads);
+ return out_proj->forward(ctx, out);
+ }
+ };
+
+ struct SigVQBlock : public GGMLBlock {
+ public:
+ SigVQBlock(const SigVQConfig& config) {
+ blocks["norm1"] = std::make_shared(config.hidden_size, config.norm_eps);
+ blocks["norm2"] = std::make_shared(config.hidden_size, config.norm_eps);
+ blocks["attn"] = std::make_shared(config);
+ blocks["mlp.fc1"] = std::make_shared(config.hidden_size, config.intermediate_size, true);
+ blocks["mlp.fc2"] = std::make_shared(config.intermediate_size, config.hidden_size, true);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx, ggml_tensor* x) {
+ auto norm1 = std::dynamic_pointer_cast(blocks["norm1"]);
+ auto norm2 = std::dynamic_pointer_cast(blocks["norm2"]);
+ auto attn = std::dynamic_pointer_cast(blocks["attn"]);
+ auto fc1 = std::dynamic_pointer_cast(blocks["mlp.fc1"]);
+ auto fc2 = std::dynamic_pointer_cast(blocks["mlp.fc2"]);
+
+ x = ggml_add(ctx->ggml_ctx, x, attn->forward(ctx, norm1->forward(ctx, x)));
+ auto h = fc1->forward(ctx, norm2->forward(ctx, x));
+ h = ggml_gelu_erf(ctx->ggml_ctx, h);
+ h = fc2->forward(ctx, h);
+ return ggml_add(ctx->ggml_ctx, x, h);
+ }
+ };
+
+ struct SigVQModel : public GGMLBlock {
+ protected:
+ SigVQConfig config;
+
+ public:
+ SigVQModel() = default;
+ SigVQModel(const SigVQConfig& config)
+ : config(config) {
+ blocks["visual.patch_embed.proj"] = std::make_shared(config.in_channels,
+ config.hidden_size,
+ std::make_pair(config.patch_size, config.patch_size),
+ std::make_pair(config.patch_size, config.patch_size));
+ for (int i = 0; i < config.num_layers; i++) {
+ blocks["visual.blocks." + std::to_string(i)] = std::make_shared(config);
+ }
+ blocks["vqmodel.quant_conv"] = std::make_shared(config.hidden_size,
+ config.codebook_embed_dim,
+ std::make_pair(1, 1));
+ blocks["prior_projector.net.0.proj"] = std::make_shared(config.semantic_embed_dim, config.semantic_embed_dim, true);
+ blocks["prior_projector.net.2"] = std::make_shared(config.semantic_embed_dim, config.semantic_embed_dim, true);
+ }
+
+ void init_params(ggml_context* ctx,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "") override {
+ params["visual.embeddings.position_embedding.weight"] =
+ ggml_new_tensor_2d(ctx, GGML_TYPE_F32, config.hidden_size, (config.image_size / config.patch_size) * (config.image_size / config.patch_size));
+ params["vqmodel.quantize.embedding.weight"] =
+ ggml_new_tensor_2d(ctx, GGML_TYPE_F32, config.codebook_embed_dim, config.codebook_size);
+ params["prior_token_embedding.weight"] =
+ ggml_new_tensor_2d(ctx, GGML_TYPE_F32, config.semantic_embed_dim, config.codebook_size);
+ }
+
+ // Bilinear-resamples the square position-embedding grid onto the image's patch grid.
+ // The reference uses grid_sample(align_corners=False, padding_mode="border"); the source
+ // coordinate for output index j is therefore (j + 0.5) * side / out - 0.5, clamped.
+ ggml_tensor* resample_pos_embed(GGMLRunnerContext* ctx,
+ ggml_tensor* pos_idx,
+ ggml_tensor* pos_weight) {
+ auto pos_embed = params["visual.embeddings.position_embedding.weight"];
+ auto gathered = ggml_get_rows(ctx->ggml_ctx, pos_embed, pos_idx);
+ return ggml_mul(ctx->ggml_ctx, gathered, pos_weight);
+ }
+
+ ggml_tensor* forward(GGMLRunnerContext* ctx,
+ ggml_tensor* pixel_values,
+ const std::vector& pos_idx,
+ const std::vector& pos_weight) {
+ // pixel_values: [N, in_channels, H, W] -> [N, grid_h * grid_w, semantic_embed_dim]
+ ggml_context* gctx = ctx->ggml_ctx;
+
+ auto patch_embed = std::dynamic_pointer_cast(blocks["visual.patch_embed.proj"]);
+ auto quant_conv = std::dynamic_pointer_cast(blocks["vqmodel.quant_conv"]);
+ auto proj_0 = std::dynamic_pointer_cast(blocks["prior_projector.net.0.proj"]);
+ auto proj_2 = std::dynamic_pointer_cast(blocks["prior_projector.net.2"]);
+
+ auto x = patch_embed->forward(ctx, pixel_values); // [N, hidden_size, grid_h, grid_w]
+ int64_t grid_w = x->ne[0];
+ int64_t grid_h = x->ne[1];
+ int64_t n_token = grid_h * grid_w;
+ int64_t N = x->ne[3];
+
+ x = ggml_reshape_3d(gctx, x, n_token, config.hidden_size, N);
+ x = ggml_cont(gctx, ggml_permute(gctx, x, 1, 0, 2, 3)); // [N, n_token, hidden_size]
+
+ ggml_tensor* pos = nullptr;
+ for (size_t i = 0; i < pos_idx.size(); i++) {
+ auto corner = resample_pos_embed(ctx, pos_idx[i], pos_weight[i]);
+ pos = pos == nullptr ? corner : ggml_add(gctx, pos, corner);
+ }
+ x = ggml_add(gctx, x, ggml_reshape_3d(gctx, pos, config.hidden_size, n_token, N));
+
+ for (int i = 0; i < config.num_layers; i++) {
+ auto block = std::dynamic_pointer_cast(blocks["visual.blocks." + std::to_string(i)]);
+ x = block->forward(ctx, x);
+ }
+
+ // quant_conv is 1x1, so run it as a per-token projection rather than reshaping to 2-D.
+ x = ggml_cont(gctx, ggml_permute(gctx, x, 1, 0, 2, 3)); // [N, hidden_size, n_token]
+ x = ggml_reshape_4d(gctx, x, n_token, 1, config.hidden_size, N);
+ x = quant_conv->forward(ctx, x); // [N, codebook_embed_dim, 1, n_token]
+ x = ggml_reshape_3d(gctx, x, n_token, config.codebook_embed_dim, N);
+ x = ggml_cont(gctx, ggml_permute(gctx, x, 1, 0, 2, 3)); // [N, n_token, codebook_embed_dim]
+
+ // Both sides are L2-normalized, so the nearest codebook entry by euclidean distance
+ // is the one with the largest dot product.
+ auto codebook = ggml_l2_norm(gctx, params["vqmodel.quantize.embedding.weight"], 1e-12f);
+ auto normed = ggml_l2_norm(gctx, x, 1e-12f);
+ auto logits = ggml_mul_mat(gctx, codebook, normed); // [N, n_token, codebook_size]
+ auto token_ids = ggml_argmax(gctx, ggml_reshape_2d(gctx, logits, config.codebook_size, n_token * N));
+
+ auto semantic = ggml_get_rows(gctx, params["prior_token_embedding.weight"], token_ids);
+ semantic = ggml_reshape_3d(gctx, semantic, config.semantic_embed_dim, n_token, N);
+
+ auto h = proj_0->forward(ctx, semantic);
+ h = ggml_silu(gctx, h);
+ return proj_2->forward(ctx, h);
+ }
+ };
+
+ struct QueryFormerRunner : public GGMLRunner {
+ public:
+ QueryFormerConfig config;
+ QueryFormerModel query_former;
+
+ QueryFormerRunner(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "",
+ std::shared_ptr weight_manager = nullptr)
+ : GGMLRunner(backend, weight_manager) {
+ query_former = QueryFormerModel(config);
+ query_former.init(params_ctx, tensor_storage_map, prefix);
+ }
+
+ std::string get_desc() override {
+ return "llada_image_queryformer";
+ }
+
+ void get_param_tensors(std::map& tensors, const std::string& prefix) {
+ query_former.get_param_tensors(tensors, prefix);
+ }
+
+ sd::Tensor compute(int n_threads, const sd::Tensor& inputs_embeds) {
+ auto get_graph = [&]() -> ggml_cgraph* {
+ ggml_cgraph* gf = new_graph_custom(LLADA_IMAGE_TE_GRAPH_SIZE);
+ ggml_tensor* x = make_input(inputs_embeds);
+ auto runner_ctx = get_context();
+ ggml_tensor* out = query_former.forward(&runner_ctx, x);
+ ggml_build_forward_expand(gf, out);
+ return gf;
+ };
+ return restore_trailing_singleton_dims(GGMLRunner::compute(get_graph, n_threads, true),
+ inputs_embeds.dim());
+ }
+ };
+
+ struct TextProjectionRunner : public GGMLRunner {
+ public:
+ TextProjectionConfig config;
+ TextProjectionModel text_projection;
+
+ TextProjectionRunner(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "",
+ std::shared_ptr weight_manager = nullptr)
+ : GGMLRunner(backend, weight_manager) {
+ text_projection = TextProjectionModel(config);
+ text_projection.init(params_ctx, tensor_storage_map, prefix);
+ }
+
+ std::string get_desc() override {
+ return "llada_image_text_projection";
+ }
+
+ void get_param_tensors(std::map& tensors, const std::string& prefix) {
+ text_projection.get_param_tensors(tensors, prefix);
+ }
+
+ sd::Tensor compute(int n_threads, const sd::Tensor& hidden_states) {
+ auto get_graph = [&]() -> ggml_cgraph* {
+ ggml_cgraph* gf = new_graph_custom(LLADA_IMAGE_TE_GRAPH_SIZE);
+ ggml_tensor* x = make_input(hidden_states);
+ auto runner_ctx = get_context();
+ ggml_tensor* out = text_projection.forward(&runner_ctx, x);
+ ggml_build_forward_expand(gf, out);
+ return gf;
+ };
+ return restore_trailing_singleton_dims(GGMLRunner::compute(get_graph, n_threads, true),
+ hidden_states.dim());
+ }
+ };
+
+ struct SigVQRunner : public GGMLRunner {
+ public:
+ SigVQConfig config;
+ SigVQModel sigvq;
+ std::array, 4> pos_idx_data;
+ std::array, 4> pos_weight_data;
+
+ SigVQRunner(ggml_backend_t backend,
+ const String2TensorStorage& tensor_storage_map = {},
+ const std::string prefix = "",
+ std::shared_ptr weight_manager = nullptr)
+ : GGMLRunner(backend, weight_manager) {
+ sigvq = SigVQModel(config);
+ sigvq.init(params_ctx, tensor_storage_map, prefix);
+ }
+
+ std::string get_desc() override {
+ return "llada_image_sigvq";
+ }
+
+ void get_param_tensors(std::map& tensors, const std::string& prefix) {
+ sigvq.get_param_tensors(tensors, prefix);
+ }
+
+ // Precomputes the four bilinear taps that resample the square position-embedding grid
+ // onto a grid_h x grid_w patch grid, matching grid_sample(align_corners=False,
+ // padding_mode="border").
+ void build_pos_embed_taps(int64_t grid_h, int64_t grid_w) {
+ const int64_t side = config.image_size / config.patch_size;
+ for (auto& v : pos_idx_data) {
+ v.clear();
+ }
+ for (auto& v : pos_weight_data) {
+ v.clear();
+ }
+
+ auto clamp_index = [side](int64_t v) {
+ return static_cast(std::min(std::max(v, 0), side - 1));
+ };
+
+ for (int64_t i = 0; i < grid_h; ++i) {
+ double src_h = (static_cast(i) + 0.5) * side / static_cast