From f739158f3eb11e016e87bfadfa4a965ce90c695e Mon Sep 17 00:00:00 2001 From: vmobilis <75476228+vmobilis@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:19:35 +0300 Subject: [PATCH 1/5] use `sd_get_preview_interval()` --- src/stable-diffusion.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/stable-diffusion.cpp b/src/stable-diffusion.cpp index 109a2a483..7370f6a03 100644 --- a/src/stable-diffusion.cpp +++ b/src/stable-diffusion.cpp @@ -2670,7 +2670,9 @@ class StableDiffusionGGML { denoised = denoised * denoise_mask + init_latent * (1.0f - denoise_mask); } if (sd_should_preview_denoised() && preview.callback != nullptr) { - preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); + if (step % sd_get_preview_interval() == 0) { + preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); + } } report_sample_progress(step, steps, &last_progress_us); sd::guidance::GuiderOutput output; @@ -2679,7 +2681,9 @@ class StableDiffusionGGML { } if (sd_should_preview_noisy() && preview.callback != nullptr) { - preview_image(step, noised_input, version, preview.mode, preview.callback, preview.data, true); + if (step % sd_get_preview_interval() == 0) { + preview_image(step, noised_input, version, preview.mode, preview.callback, preview.data, true); + } } sd::Tensor cond_out; @@ -2889,7 +2893,9 @@ class StableDiffusionGGML { denoised = denoised * denoise_mask + init_latent * (1.0f - denoise_mask); } if (sd_should_preview_denoised() && preview.callback != nullptr) { - preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); + if (step % sd_get_preview_interval() == 0) { + preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); + } } report_sample_progress(step, steps, &last_progress_us); output.pred = denoised; From cb2d258440785ac33db27984ffbf38c9eb5d38bc Mon Sep 17 00:00:00 2001 From: vmobilis <75476228+vmobilis@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:31:06 +0300 Subject: [PATCH 2/5] Options for `preview-interval` --- src/stable-diffusion.cpp | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/stable-diffusion.cpp b/src/stable-diffusion.cpp index 7370f6a03..ddf4840e2 100644 --- a/src/stable-diffusion.cpp +++ b/src/stable-diffusion.cpp @@ -259,6 +259,8 @@ class StableDiffusionGGML { bool is_using_v_parameterization = false; bool is_using_edm_v_parameterization = false; + bool preview_last_step = false; + size_t control_net_params_mem_size = 0; std::shared_ptr model_manager; @@ -2639,6 +2641,18 @@ class StableDiffusionGGML { float c_out = scaling[1]; float c_in = scaling[2]; + bool preview_needed = preview.callback != nullptr; + if (preview_needed) { + int preview_interval = sd_get_preview_interval(); + if (preview_interval > 0) { // every Nth step + preview_needed = step % preview_interval == 0; + } else if (preview_interval < 0) { // only (-N)th step + preview_needed = step == -preview_interval; + } else { // last step of base resolution / high noise pass + preview_needed = preview_last_step && step == steps; + } + } + std::vector base_timesteps_vec = prepare_sample_timesteps(sigma, shifted_timestep); std::vector timesteps_vec = base_timesteps_vec; sd::Tensor audio_timesteps_tensor; @@ -2669,10 +2683,8 @@ class StableDiffusionGGML { if (!denoise_mask.empty()) { denoised = denoised * denoise_mask + init_latent * (1.0f - denoise_mask); } - if (sd_should_preview_denoised() && preview.callback != nullptr) { - if (step % sd_get_preview_interval() == 0) { - preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); - } + if (preview_needed && sd_should_preview_denoised()) { + preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); } report_sample_progress(step, steps, &last_progress_us); sd::guidance::GuiderOutput output; @@ -2680,10 +2692,8 @@ class StableDiffusionGGML { return output; } - if (sd_should_preview_noisy() && preview.callback != nullptr) { - if (step % sd_get_preview_interval() == 0) { - preview_image(step, noised_input, version, preview.mode, preview.callback, preview.data, true); - } + if (preview_needed && sd_should_preview_noisy()) { + preview_image(step, noised_input, version, preview.mode, preview.callback, preview.data, true); } sd::Tensor cond_out; @@ -2892,10 +2902,8 @@ class StableDiffusionGGML { if (!denoise_mask.empty()) { denoised = denoised * denoise_mask + init_latent * (1.0f - denoise_mask); } - if (sd_should_preview_denoised() && preview.callback != nullptr) { - if (step % sd_get_preview_interval() == 0) { - preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); - } + if (preview_needed && sd_should_preview_denoised()) { + preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); } report_sample_progress(step, steps, &last_progress_us); output.pred = denoised; @@ -5666,6 +5674,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, ImageGenerationEmbeds embeds = std::move(*embeds_opt); std::vector> final_latents; + sd_ctx->sd->preview_last_step = true; int64_t denoise_start = ggml_time_ms(); for (int b = 0; b < request.batch_count; b++) { sd_cancel_mode_t cancel = sd_ctx->sd->get_cancel_flag(); @@ -5726,6 +5735,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, return false; } int64_t denoise_end = ggml_time_ms(); + sd_ctx->sd->preview_last_step = false; LOG_INFO("generating %zu latent images completed, taking %.2fs", final_latents.size(), (denoise_end - denoise_start) * 1.0f / 1000); @@ -6938,6 +6948,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, } LOG_DEBUG("sample(high noise) %dx%dx%d", W, H, T); + sd_ctx->sd->preview_last_step = true; int64_t sampling_start = ggml_time_ms(); std::vector high_noise_sigmas(plan.sigmas.begin(), plan.sigmas.begin() + plan.high_noise_sample_steps + 1); plan.sigmas = std::vector(plan.sigmas.begin() + plan.high_noise_sample_steps, plan.sigmas.end()); @@ -6968,6 +6979,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, request.cache_params, latents.video_positions); int64_t sampling_end = ggml_time_ms(); + sd_ctx->sd->preview_last_step = false; if (x_t_sampled.empty()) { LOG_ERROR("sampling(high noise) failed after %.2fs", (sampling_end - sampling_start) * 1.0f / 1000); return false; From 1c6b83e56029898fc3f41b531c0762cd3258bc01 Mon Sep 17 00:00:00 2001 From: vmobilis <75476228+vmobilis@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:36:34 +0300 Subject: [PATCH 3/5] Description of preview options --- examples/cli/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 953a44e9d..249a6a551 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -94,7 +94,7 @@ struct SDCliParams { options.int_options = { {"", "--preview-interval", - "interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step)", + "interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step). Set to -N to preview only the Nth single step. Set to 0 to preview the last step of 1st pass (base resolution / high noise) sampling", &preview_interval}, {"", "--output-begin-idx", From 5a8944480e35dc3c98ada20d660f31b96aa0b6db Mon Sep 17 00:00:00 2001 From: vmobilis <75476228+vmobilis@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:56:08 +0300 Subject: [PATCH 4/5] stable-diffusion.cpp: clang-tidy formatting --- src/stable-diffusion.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stable-diffusion.cpp b/src/stable-diffusion.cpp index ddf4840e2..8a4022cd0 100644 --- a/src/stable-diffusion.cpp +++ b/src/stable-diffusion.cpp @@ -2644,11 +2644,11 @@ class StableDiffusionGGML { bool preview_needed = preview.callback != nullptr; if (preview_needed) { int preview_interval = sd_get_preview_interval(); - if (preview_interval > 0) { // every Nth step + if (preview_interval > 0) { // every Nth step preview_needed = step % preview_interval == 0; } else if (preview_interval < 0) { // only (-N)th step preview_needed = step == -preview_interval; - } else { // last step of base resolution / high noise pass + } else { // last step of base resolution / high noise pass preview_needed = preview_last_step && step == steps; } } @@ -5675,7 +5675,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, std::vector> final_latents; sd_ctx->sd->preview_last_step = true; - int64_t denoise_start = ggml_time_ms(); + int64_t denoise_start = ggml_time_ms(); for (int b = 0; b < request.batch_count; b++) { sd_cancel_mode_t cancel = sd_ctx->sd->get_cancel_flag(); if (cancel == SD_CANCEL_ALL) { @@ -5734,7 +5734,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, (sampling_end - sampling_start) * 1.0f / 1000); return false; } - int64_t denoise_end = ggml_time_ms(); + int64_t denoise_end = ggml_time_ms(); sd_ctx->sd->preview_last_step = false; LOG_INFO("generating %zu latent images completed, taking %.2fs", final_latents.size(), @@ -6949,7 +6949,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, LOG_DEBUG("sample(high noise) %dx%dx%d", W, H, T); sd_ctx->sd->preview_last_step = true; - int64_t sampling_start = ggml_time_ms(); + int64_t sampling_start = ggml_time_ms(); std::vector high_noise_sigmas(plan.sigmas.begin(), plan.sigmas.begin() + plan.high_noise_sample_steps + 1); plan.sigmas = std::vector(plan.sigmas.begin() + plan.high_noise_sample_steps, plan.sigmas.end()); From 130bd85741f997c7b7da153f4fb6630ad5e9053b Mon Sep 17 00:00:00 2001 From: leejet Date: Sun, 30 Aug 2026 21:46:17 +0800 Subject: [PATCH 5/5] fix: handle preview intervals across sampling passes --- examples/cli/main.cpp | 2 +- include/stable-diffusion.h | 3 ++ src/runtime/preview_interval.h | 45 +++++++++++++++++++++++++++ src/stable-diffusion.cpp | 56 +++++++++++++++++----------------- 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 src/runtime/preview_interval.h diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 249a6a551..43719abbc 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -94,7 +94,7 @@ struct SDCliParams { options.int_options = { {"", "--preview-interval", - "interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at every step). Set to -N to preview only the Nth single step. Set to 0 to preview the last step of 1st pass (base resolution / high noise) sampling", + "preview interval: in each sampling pass, positive N updates every Nth denoiser step and -N previews only completed logical step N; 0 previews the final completed step of the first pass (base-resolution or high-noise). Default: 1", &preview_interval}, {"", "--output-begin-idx", diff --git a/include/stable-diffusion.h b/include/stable-diffusion.h index 6bea445ab..d5cda6d9a 100644 --- a/include/stable-diffusion.h +++ b/include/stable-diffusion.h @@ -446,6 +446,9 @@ typedef bool (*sd_graph_eval_callback_t)(struct ggml_tensor* t, bool ask, void* SD_API void sd_set_log_callback(sd_log_cb_t sd_log_cb, void* data); SD_API void sd_set_progress_callback(sd_progress_cb_t cb, void* data); +// In each sampling pass, a positive interval previews every Nth denoiser step, while a +// negative interval previews only completed logical step -interval. Zero previews the final +// completed step of the first sampling pass (base-resolution or high-noise). SD_API void sd_set_preview_callback(sd_preview_cb_t cb, enum preview_t mode, int interval, bool denoised, bool noisy, void* data); SD_API void sd_set_backend_eval_callback(sd_graph_eval_callback_t cb, void* data); SD_API int32_t sd_get_num_physical_cores(); diff --git a/src/runtime/preview_interval.h b/src/runtime/preview_interval.h new file mode 100644 index 000000000..aab997682 --- /dev/null +++ b/src/runtime/preview_interval.h @@ -0,0 +1,45 @@ +#ifndef __SD_RUNTIME_PREVIEW_INTERVAL_H__ +#define __SD_RUNTIME_PREVIEW_INTERVAL_H__ + +#include +#include +#include + +namespace sd::preview { + + constexpr std::uint64_t logical_sample_step(int step) { + return step < 0 ? static_cast(-static_cast(step)) + : static_cast(step); + } + + constexpr bool sample_step_is_complete(int step, + std::size_t total_steps, + bool terminal_sigma_is_zero) { + return step > 0 || + (terminal_sigma_is_zero && + step < 0 && + logical_sample_step(step) == static_cast(total_steps)); + } + + constexpr bool should_preview_sample_step(int step, + std::size_t total_steps, + bool terminal_sigma_is_zero, + int interval, + bool preview_final_step) { + if (interval > 0) { + return step % interval == 0; + } + if (!sample_step_is_complete(step, total_steps, terminal_sigma_is_zero)) { + return false; + } + + std::uint64_t logical_step = logical_sample_step(step); + if (interval < 0) { + std::uint64_t requested_step = static_cast(-static_cast(interval)); + return logical_step == requested_step; + } + return preview_final_step && logical_step == static_cast(total_steps); + } +} // namespace sd::preview + +#endif // __SD_RUNTIME_PREVIEW_INTERVAL_H__ diff --git a/src/stable-diffusion.cpp b/src/stable-diffusion.cpp index 6e7343a2c..2c1a18b12 100644 --- a/src/stable-diffusion.cpp +++ b/src/stable-diffusion.cpp @@ -61,6 +61,7 @@ #include "model/vae/wan_vae.hpp" #include "runtime/denoiser.hpp" #include "runtime/guidance.h" +#include "runtime/preview_interval.h" #include "runtime/sample-cache.h" #include "upscaler.h" @@ -259,8 +260,6 @@ class StableDiffusionGGML { bool is_using_v_parameterization = false; bool is_using_edm_v_parameterization = false; - bool preview_last_step = false; - size_t control_net_params_mem_size = 0; std::shared_ptr model_manager; @@ -2469,8 +2468,11 @@ class StableDiffusionGGML { sd_get_preview_mode()}; } - void report_sample_progress(int step, size_t total_steps, int64_t* last_progress_us) { - if (step > 0 || step == -(int)total_steps) { + void report_sample_progress(int step, + size_t total_steps, + bool terminal_sigma_is_zero, + int64_t* last_progress_us) { + if (sd::preview::sample_step_is_complete(step, total_steps, terminal_sigma_is_zero)) { int64_t now = ggml_time_us(); int showstep = std::abs(step); float step_seconds = last_progress_us != nullptr && *last_progress_us > 0 @@ -2532,6 +2534,7 @@ class StableDiffusionGGML { int audio_length, float frame_rate, const sd_cache_params_t* cache_params, + bool preview_final_step, const sd::Tensor& video_positions = {}) { struct RunnerDoneOnExit { GGMLRunner* runner = nullptr; @@ -2591,8 +2594,9 @@ class StableDiffusionGGML { } } - size_t steps = sigmas.size() - 1; - bool has_skiplayer = (slg_scale != 0.0f || slg_uncond) && !skip_layers.empty(); + size_t steps = sigmas.size() - 1; + bool terminal_sigma_is_zero = sigmas.back() == 0.f; + bool has_skiplayer = (slg_scale != 0.0f || slg_uncond) && !skip_layers.empty(); if (has_skiplayer && !sd_version_is_dit(version)) { has_skiplayer = false; LOG_WARN("SLG is incompatible with this model type"); @@ -2641,17 +2645,12 @@ class StableDiffusionGGML { float c_out = scaling[1]; float c_in = scaling[2]; - bool preview_needed = preview.callback != nullptr; - if (preview_needed) { - int preview_interval = sd_get_preview_interval(); - if (preview_interval > 0) { // every Nth step - preview_needed = step % preview_interval == 0; - } else if (preview_interval < 0) { // only (-N)th step - preview_needed = step == -preview_interval; - } else { // last step of base resolution / high noise pass - preview_needed = preview_last_step && step == steps; - } - } + bool preview_needed = preview.callback != nullptr && + sd::preview::should_preview_sample_step(step, + steps, + terminal_sigma_is_zero, + sd_get_preview_interval(), + preview_final_step); std::vector base_timesteps_vec = prepare_sample_timesteps(sigma, shifted_timestep); std::vector timesteps_vec = base_timesteps_vec; @@ -2686,7 +2685,7 @@ class StableDiffusionGGML { if (preview_needed && sd_should_preview_denoised()) { preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); } - report_sample_progress(step, steps, &last_progress_us); + report_sample_progress(step, steps, terminal_sigma_is_zero, &last_progress_us); sd::guidance::GuiderOutput output; output.pred = denoised; return output; @@ -2909,7 +2908,7 @@ class StableDiffusionGGML { if (preview_needed && sd_should_preview_denoised()) { preview_image(step, denoised, version, preview.mode, preview.callback, preview.data, false); } - report_sample_progress(step, steps, &last_progress_us); + report_sample_progress(step, steps, terminal_sigma_is_zero, &last_progress_us); output.pred = denoised; return output; }; @@ -5682,8 +5681,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, ImageGenerationEmbeds embeds = std::move(*embeds_opt); std::vector> final_latents; - sd_ctx->sd->preview_last_step = true; - int64_t denoise_start = ggml_time_ms(); + int64_t denoise_start = ggml_time_ms(); for (int b = 0; b < request.batch_count; b++) { sd_cancel_mode_t cancel = sd_ctx->sd->get_cancel_flag(); if (cancel == SD_CANCEL_ALL) { @@ -5728,7 +5726,8 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, 1.f, 0, static_cast(request.fps), - request.cache_params); + request.cache_params, + true); int64_t sampling_end = ggml_time_ms(); if (!x_0.empty()) { LOG_INFO("sampling completed, taking %.2fs", (sampling_end - sampling_start) * 1.0f / 1000); @@ -5742,8 +5741,7 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, (sampling_end - sampling_start) * 1.0f / 1000); return false; } - int64_t denoise_end = ggml_time_ms(); - sd_ctx->sd->preview_last_step = false; + int64_t denoise_end = ggml_time_ms(); LOG_INFO("generating %zu latent images completed, taking %.2fs", final_latents.size(), (denoise_end - denoise_start) * 1.0f / 1000); @@ -5850,7 +5848,8 @@ SD_API bool generate_image(sd_ctx_t* sd_ctx, 1.f, 0, static_cast(request.fps), - request.cache_params); + request.cache_params, + false); int64_t hires_sample_end = ggml_time_ms(); if (!x_0.empty()) { LOG_INFO("hires sampling %d/%d completed, taking %.2fs", @@ -6956,8 +6955,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, } LOG_DEBUG("sample(high noise) %dx%dx%d", W, H, T); - sd_ctx->sd->preview_last_step = true; - int64_t sampling_start = ggml_time_ms(); + int64_t sampling_start = ggml_time_ms(); std::vector high_noise_sigmas(plan.sigmas.begin(), plan.sigmas.begin() + plan.high_noise_sample_steps + 1); plan.sigmas = std::vector(plan.sigmas.begin() + plan.high_noise_sample_steps, plan.sigmas.end()); @@ -6985,9 +6983,9 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, latents.audio_length, static_cast(request.fps), request.cache_params, + true, latents.video_positions); int64_t sampling_end = ggml_time_ms(); - sd_ctx->sd->preview_last_step = false; if (x_t_sampled.empty()) { LOG_ERROR("sampling(high noise) failed after %.2fs", (sampling_end - sampling_start) * 1.0f / 1000); return false; @@ -7028,6 +7026,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, latents.audio_length, static_cast(request.fps), request.cache_params, + plan.high_noise_sample_steps <= 0, latents.video_positions); int64_t sampling_end = ggml_time_ms(); @@ -7166,6 +7165,7 @@ SD_API bool generate_video(sd_ctx_t* sd_ctx, latents.audio_length, static_cast(hires_request.fps), hires_request.cache_params, + false, hires_video_positions); sampling_end = ggml_time_ms(); if (final_latent.empty()) {