From 281d15b4a1ff4ad84ff1aaf57dce9b92a34c7db5 Mon Sep 17 00:00:00 2001 From: Aleksandr Istomin Date: Sat, 15 Aug 2026 13:24:27 +0100 Subject: [PATCH] Preview the denoised estimate rather than the noisy sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Denoising previews decode the sample the sampler is stepping through. At high sigma that sample genuinely is mostly noise, so on a short schedule the preview shows near-identical coloured latent mush for most of the run and only resolves on the last step — which defeats the point of a preview, since there is nothing to judge and nothing to cancel on. Decoding the model's current estimate of the finished frame instead gives a blurry version of the result from the first step, which then sharpens. The estimate is x0 = x_t + sigma * v, the same expression denoise_res already uses to build its denoised buffer; note the sign, because the Euler step moves the sample along +v as sigma falls, so this engine's velocity points toward the clean image rather than away from it. The estimate is written to a scratch buffer, so the sampler's own trajectory is untouched and final output is unchanged — verified bit-identical at SSIM 1.000000 against the same seed before and after. denoise_euler_gpu still previews the sample: its velocity lives in a BF16 GPU tensor and would need a separate readback to combine, and that path selects on M5-class hardware, which I could not test on. The comment records that rather than leaving it silently inconsistent. Measured on an M1 Pro with a turbo checkpoint at 8 passes: previews go from unusable to recognisable at step 1. --- h3_dit.c | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/h3_dit.c b/h3_dit.c index 667e49b2..81c51525 100644 --- a/h3_dit.c +++ b/h3_dit.c @@ -2740,6 +2740,12 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, } } if (ok && preview) { + /* This path previews the sample rather than the denoised + * estimate the CPU sampler shows: the velocity lives in a BF16 + * GPU tensor here and would need its own readback to combine. + * The GPU sampler runs on M5-class hardware only, which this + * change could not be exercised on, so the cheaper preview + * stands until it can be tested there. */ ok = h3_gpu_tensor_read_f32_range( dit->video_input, video_offset, video_rows, video_count) && h3_dit_unpatchify_video( @@ -2858,6 +2864,24 @@ int h3_dit_denoise(h3_dit *dit, float *video_latent, float *audio_latent, return ok; } +/* Previews want the model's current estimate of the finished picture, not + * the sample it is stepping through. This engine's velocity points from the + * sample toward the clean image as sigma falls, so the estimate is + * x0 = x_t + sigma * v — the same expression the res sampler already uses to + * build its denoised buffer. It reads as a blurry version of the result from + * the first step and sharpens; decoding x_t instead shows latent noise until + * sigma collapses near the end, which is useless for deciding whether to + * cancel. Returns the buffer to hand the preview callback. */ +static const float *preview_estimate(float *destination, const float *sample, + const float *velocity, size_t count, + float sigma) { + if (!destination || !velocity || !isfinite(sigma) || sigma <= 0.0f) + return sample; + for (size_t index = 0; index < count; index++) + destination[index] = sample[index] + sigma * velocity[index]; + return destination; +} + int h3_dit_denoise_euler_preview( h3_dit *dit, float *video_latent, float *audio_latent, int reuse_interval, @@ -2895,6 +2919,8 @@ int h3_dit_denoise_euler_preview( size_t audio_count = h3_dit_audio_elements(dit); float *video_velocity = malloc(video_count * sizeof(*video_velocity)); float *audio_velocity = malloc(audio_count * sizeof(*audio_velocity)); + float *preview_video = preview + ? malloc(video_count * sizeof(*preview_video)) : NULL; float *last_video = reuse_interval > 1 ? malloc(video_count * sizeof(*last_video)) : NULL; float *previous_video = reuse_interval > 1 @@ -2909,6 +2935,7 @@ int h3_dit_denoise_euler_preview( fail(error, error_size, "out of memory allocating Euler velocities"); free(video_velocity); free(audio_velocity); + free(preview_video); free(last_video); free(previous_video); free(last_audio); @@ -2964,8 +2991,11 @@ int h3_dit_denoise_euler_preview( "Euler solver rejected step %d", step); } if (ok && preview && - preview(step + 1, dit->sigmas.steps, video_latent, video_count, - preview_opaque)) { + preview(step + 1, dit->sigmas.steps, + preview_estimate(preview_video, video_latent, + video_velocity, video_count, + dit->sigmas.video[step + 1]), + video_count, preview_opaque)) { fail(error, error_size, "denoising preview stopped at step %d", step + 1); ok = 0; @@ -2975,6 +3005,7 @@ int h3_dit_denoise_euler_preview( } free(video_velocity); free(audio_velocity); + free(preview_video); free(last_video); free(previous_video); free(last_audio);