From a830fb517d3eec4bd5851a59e643918e3504c1de Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Aug 2026 00:20:16 +0200 Subject: [PATCH 1/5] Add resumable Euler denoising control --- h3_dit.c | 111 +++++++++++++++++++++++++++++++++++++++++++++++++------ h3_dit.h | 27 ++++++++++++++ 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/h3_dit.c b/h3_dit.c index 9602a8a..76f11e1 100644 --- a/h3_dit.c +++ b/h3_dit.c @@ -9,6 +9,7 @@ #include #include #include +#include enum { TEXT_DIM = 5120, @@ -2284,6 +2285,20 @@ static int gpu_sampler_requested(const h3_dit *dit) { return h3_gpu_is_m5(dit->gpu); } +static double monotonic_seconds(void) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0.0; + return (double)now.tv_sec + (double)now.tv_nsec / 1e9; +} + +static int progressive_should_stop(const h3_dit_progressive *control, + int completed_steps, double elapsed) { + return (control->stop_after_seconds > 0.0 && + elapsed >= control->stop_after_seconds) || + (control->stop_after_step > 0 && + completed_steps >= control->stop_after_step); +} + static unsigned gpu_sampler_window(void) { const char *value = getenv("H3_GPU_SAMPLER_WINDOW"); if (!value || !*value) return 1; @@ -2313,6 +2328,7 @@ static int ensure_previous_velocities(h3_dit *dit, char *error, static int denoise_euler_gpu(h3_dit *dit, float *video_latent, float *audio_latent, int reuse_interval, h3_dit_progress progress, void *progress_opaque, + h3_dit_progressive *control, h3_dit_preview preview, void *preview_opaque, char *error, size_t error_size) { uint8_t selected[H3_MAX_STEPS] = {0}; @@ -2330,6 +2346,13 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, if (reuse_interval > 1 && getenv("H3_PROFILE")) fprintf(stderr, "h3: %s GPU reuse schedule has %d evaluations\n", custom_count > 0 ? "custom" : "selected", selected_count); + int start_step = control->start_step; + int checkpoint_enabled = control->stop_after_seconds > 0.0 || + control->stop_after_step > 0; + double began = monotonic_seconds(); + control->completed_steps = start_step; + control->stopped = 0; + control->elapsed_seconds = 0.0; unsigned window = gpu_sampler_window(); int disable_command_split = window == 1 && getenv("H3_DIT_COMMAND_BLOCKS") == NULL; @@ -2370,7 +2393,8 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, int previous_evaluated = -1; unsigned pending_evaluations = 0; int command_active = 0; - for (int step = 0; step < dit->sigmas.steps && ok; step++) { + int completed_steps = start_step; + for (int step = start_step; step < dit->sigmas.steps && ok; step++) { report(progress, progress_opaque, "denoise enqueue", step, dit->sigmas.steps); if (!command_active) { @@ -2426,8 +2450,9 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, dit->audio_output_bf16, previous_audio, (uint32_t)audio_count, dit->sigmas.audio[step] - dit->sigmas.audio[step + 1], audio_ratio), error, error_size, "GPU audio Euler step"); - if (ok && (evaluate || preview)) { - int finish = preview || step + 1 == dit->sigmas.steps || + if (ok && (evaluate || preview || checkpoint_enabled)) { + int finish = preview || checkpoint_enabled || + step + 1 == dit->sigmas.steps || (window && pending_evaluations >= window); ok = gpu_op(dit, finish ? h3_gpu_submit(dit->gpu) : h3_gpu_continue(dit->gpu), @@ -2456,8 +2481,18 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, ok = 0; } } - if (ok) report(progress, progress_opaque, "denoise enqueue", step + 1, - dit->sigmas.steps); + if (ok) { + completed_steps = step + 1; + report(progress, progress_opaque, "denoise enqueue", + completed_steps, dit->sigmas.steps); + double elapsed = monotonic_seconds() - began; + if (checkpoint_enabled && + progressive_should_stop(control, completed_steps, elapsed) && + completed_steps < dit->sigmas.steps) { + control->stopped = 1; + break; + } + } } if (ok && command_active) ok = gpu_op(dit, h3_gpu_submit(dit->gpu), error, error_size, @@ -2478,7 +2513,9 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, fail(error, error_size, "cannot unpack GPU Euler latents"); free(video_rows); free(audio_rows); - if (ok) report(progress, progress_opaque, "denoise", dit->sigmas.steps, + control->completed_steps = completed_steps; + control->elapsed_seconds = monotonic_seconds() - began; + if (ok) report(progress, progress_opaque, "denoise", completed_steps, dit->sigmas.steps); h3_gpu_profile_mark(dit->gpu, "GPU Euler denoise"); return ok; @@ -2558,22 +2595,39 @@ int h3_dit_denoise(h3_dit *dit, float *video_latent, float *audio_latent, return ok; } -int h3_dit_denoise_euler_preview( +int h3_dit_denoise_euler_progressive( h3_dit *dit, float *video_latent, float *audio_latent, int reuse_interval, h3_dit_progress progress, void *progress_opaque, + h3_dit_progressive *control, h3_dit_preview preview, void *preview_opaque, char *error, size_t error_size) { if (error && error_size) error[0] = '\0'; - if (!dit || !video_latent || !audio_latent || reuse_interval < 1 || + if (!dit || !video_latent || !audio_latent || !control || + reuse_interval < 1 || reuse_interval > 32 || - dit->sigmas.steps != h3_dit_schedule_steps(dit->schedule)) { + dit->sigmas.steps != h3_dit_schedule_steps(dit->schedule) || + control->start_step < 0 || + control->start_step >= dit->sigmas.steps || + !isfinite(control->stop_after_seconds) || + control->stop_after_seconds < 0.0 || + control->stop_after_step < 0 || + control->stop_after_step >= dit->sigmas.steps || + (control->stop_after_step > 0 && + control->stop_after_step <= control->start_step) || + (control->stop_after_seconds > 0.0 && + control->stop_after_step > 0) || + ((control->start_step > 0 || + control->stop_after_seconds > 0.0 || + control->stop_after_step > 0) && + (reuse_interval != 1 || dit->core_reuse_interval != 1))) { fail(error, error_size, "invalid Euler denoising arguments"); return 0; } if (gpu_sampler_requested(dit)) return denoise_euler_gpu(dit, video_latent, audio_latent, reuse_interval, progress, progress_opaque, + control, preview, preview_opaque, error, error_size); uint8_t selected[H3_MAX_STEPS] = {0}; @@ -2591,6 +2645,13 @@ int h3_dit_denoise_euler_preview( if (reuse_interval > 1 && getenv("H3_PROFILE")) fprintf(stderr, "h3: %s reuse schedule has %d evaluations\n", custom_count > 0 ? "custom" : "selected", selected_count); + int start_step = control->start_step; + int checkpoint_enabled = control->stop_after_seconds > 0.0 || + control->stop_after_step > 0; + double began = monotonic_seconds(); + control->completed_steps = start_step; + control->stopped = 0; + control->elapsed_seconds = 0.0; size_t video_count = h3_dit_video_elements(dit); size_t audio_count = h3_dit_audio_elements(dit); float *video_velocity = malloc(video_count * sizeof(*video_velocity)); @@ -2618,7 +2679,8 @@ int h3_dit_denoise_euler_preview( int ok = 1; int last_evaluated = -1; int previous_evaluated = -1; - for (int step = 0; step < dit->sigmas.steps && ok; step++) { + int completed_steps = start_step; + for (int step = start_step; step < dit->sigmas.steps && ok; step++) { report(progress, progress_opaque, "denoise", step, dit->sigmas.steps); int evaluate = selected[step]; if (evaluate) { @@ -2670,8 +2732,18 @@ int h3_dit_denoise_euler_preview( step + 1); ok = 0; } - if (ok) report(progress, progress_opaque, "denoise", step + 1, - dit->sigmas.steps); + if (ok) { + completed_steps = step + 1; + report(progress, progress_opaque, "denoise", completed_steps, + dit->sigmas.steps); + double elapsed = monotonic_seconds() - began; + if (checkpoint_enabled && + progressive_should_stop(control, completed_steps, elapsed) && + completed_steps < dit->sigmas.steps) { + control->stopped = 1; + break; + } + } } free(video_velocity); free(audio_velocity); @@ -2679,10 +2751,25 @@ int h3_dit_denoise_euler_preview( free(previous_video); free(last_audio); free(previous_audio); + control->completed_steps = completed_steps; + control->elapsed_seconds = monotonic_seconds() - began; h3_gpu_profile_mark(dit->gpu, "Euler denoise"); return ok; } +int h3_dit_denoise_euler_preview( + h3_dit *dit, float *video_latent, + float *audio_latent, int reuse_interval, + h3_dit_progress progress, void *progress_opaque, + h3_dit_preview preview, void *preview_opaque, + char *error, size_t error_size) { + h3_dit_progressive control = {0}; + return h3_dit_denoise_euler_progressive( + dit, video_latent, audio_latent, reuse_interval, + progress, progress_opaque, &control, preview, preview_opaque, + error, error_size); +} + int h3_dit_denoise_euler(h3_dit *dit, float *video_latent, float *audio_latent, int reuse_interval, h3_dit_progress progress, void *progress_opaque, diff --git a/h3_dit.h b/h3_dit.h index 688086b..4ed0950 100644 --- a/h3_dit.h +++ b/h3_dit.h @@ -17,6 +17,21 @@ typedef int (*h3_dit_preview)(int completed_steps, int total_steps, const float *video_latent, size_t video_elements, void *opaque); +typedef struct { + /* Input: first schedule transition to execute. Zero starts a new run. */ + int start_step; + /* Input: pause at the first completed transition at or after this many + * wall-clock seconds. Zero disables pausing. */ + double stop_after_seconds; + /* Input: pause after this exact number of completed transitions. Zero + * disables pausing. */ + int stop_after_step; + /* Outputs filled even when the schedule reaches its normal end. */ + int completed_steps; + int stopped; + double elapsed_seconds; +} h3_dit_progressive; + /* Load a text-only FL2VA transformer. Text refinement and AdaLN precomputation * happen before the persistent 37 GiB core is mapped, keeping phase residency * bounded on 128 GiB machines. */ @@ -116,6 +131,18 @@ int h3_dit_denoise_euler_preview( h3_dit_preview preview, void *preview_opaque, char *error, size_t error_size); +/* Resumable full-evaluation Euler path. A nonzero start_step, + * stop_after_seconds, or stop_after_step requires both denoiser and prepared + * core reuse intervals to be 1; accelerated history is not part of the v1 + * checkpoint contract. */ +int h3_dit_denoise_euler_progressive( + h3_dit *dit, float *video_latent, + float *audio_latent, int reuse_interval, + h3_dit_progress progress, void *progress_opaque, + h3_dit_progressive *control, + h3_dit_preview preview, void *preview_opaque, + char *error, size_t error_size); + /* Build the velocity-evaluation mask used by the serving sampler. Returns the * evaluation count, or -1 for invalid arguments. Public internally so the * quality-tuned aggressive schedule remains pinned by cheap host tests. */ From 3561f5ec88667ff977a320152b8aa8d75ec481b2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Aug 2026 00:22:03 +0200 Subject: [PATCH 2/5] Add atomic generation checkpoint format --- Makefile | 3 +- h3_checkpoint.c | 347 ++++++++++++++++++++++++++++++++++++++++++++++++ h3_checkpoint.h | 38 ++++++ tests/test_h3.c | 164 +++++++++++++++++++++++ 4 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 h3_checkpoint.c create mode 100644 h3_checkpoint.h diff --git a/Makefile b/Makefile index bb20237..121a437 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,8 @@ FRAMEWORKS := -framework Foundation -framework Metal \ -framework Accelerate LDLIBS := $(FRAMEWORKS) -licucore -lm -LIB_C := h3.c h3_host.c h3_safetensors.c h3_weights.c h3_text_encoder.c \ +LIB_C := h3.c h3_checkpoint.c h3_host.c h3_safetensors.c h3_weights.c \ + h3_text_encoder.c \ h3_dit_schedule.c h3_dit.c LIB_C += h3_video_vae.c h3_video_encoder.c h3_audio_vae.c h3_ffmpeg.c \ diff --git a/h3_checkpoint.c b/h3_checkpoint.c new file mode 100644 index 0000000..40f96c7 --- /dev/null +++ b/h3_checkpoint.c @@ -0,0 +1,347 @@ +#include "h3_checkpoint.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + H3_CHECKPOINT_HEADER_SIZE = 128, + H3_CHECKPOINT_ENDIAN = 0x01020304, + H3_CHECKPOINT_HASH_OFFSET = 80 +}; + +static const unsigned char h3_checkpoint_magic[8] = { + 'H', '3', 'C', 'K', 'P', 'T', '1', '\n' +}; + +static void fail(char *error, size_t error_size, const char *format, ...) { + if (!error || !error_size) return; + va_list arguments; + va_start(arguments, format); + vsnprintf(error, error_size, format, arguments); + va_end(arguments); +} + +static uint64_t hash_bytes(uint64_t hash, const void *data, size_t size) { + const unsigned char *bytes = data; + for (size_t index = 0; index < size; index++) { + hash ^= bytes[index]; + hash *= UINT64_C(1099511628211); + } + return hash; +} + +uint64_t h3_checkpoint_signature(const char *text) { + if (!text) return 0; + return hash_bytes(UINT64_C(14695981039346656037), text, strlen(text)); +} + +static void put_u32(unsigned char *destination, uint32_t value) { + for (unsigned index = 0; index < 4; index++) + destination[index] = (unsigned char)(value >> (8 * index)); +} + +static void put_u64(unsigned char *destination, uint64_t value) { + for (unsigned index = 0; index < 8; index++) + destination[index] = (unsigned char)(value >> (8 * index)); +} + +static uint32_t get_u32(const unsigned char *source) { + uint32_t value = 0; + for (unsigned index = 0; index < 4; index++) + value |= (uint32_t)source[index] << (8 * index); + return value; +} + +static uint64_t get_u64(const unsigned char *source) { + uint64_t value = 0; + for (unsigned index = 0; index < 8; index++) + value |= (uint64_t)source[index] << (8 * index); + return value; +} + +static uint64_t double_bits(double value) { + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +static double bits_double(uint64_t bits) { + double value; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static int valid_info(const h3_checkpoint_info *info) { + return info && info->signature && info->total_steps >= 2 && + info->next_step > 0 && info->next_step < info->total_steps && + info->render_width > 0 && info->render_height > 0 && + info->frame_count >= 22 && info->video_elements > 0 && + info->audio_elements > 0 && isfinite(info->denoise_seconds) && + info->denoise_seconds >= 0.0 && + info->video_elements <= SIZE_MAX / sizeof(float) && + info->audio_elements <= SIZE_MAX / sizeof(float); +} + +static void encode_header(unsigned char header[H3_CHECKPOINT_HEADER_SIZE], + const h3_checkpoint_info *info, uint64_t hash) { + memset(header, 0, H3_CHECKPOINT_HEADER_SIZE); + memcpy(header, h3_checkpoint_magic, sizeof(h3_checkpoint_magic)); + put_u32(header + 8, H3_CHECKPOINT_HEADER_SIZE); + uint32_t byte_order = H3_CHECKPOINT_ENDIAN; + memcpy(header + 12, &byte_order, sizeof(byte_order)); + put_u64(header + 16, info->signature); + put_u64(header + 24, info->seed); + put_u32(header + 32, info->total_steps); + put_u32(header + 36, info->next_step); + put_u32(header + 40, info->render_width); + put_u32(header + 44, info->render_height); + put_u32(header + 48, info->frame_count); + put_u64(header + 56, info->video_elements); + put_u64(header + 64, info->audio_elements); + put_u64(header + 72, double_bits(info->denoise_seconds)); + put_u64(header + H3_CHECKPOINT_HASH_OFFSET, hash); +} + +static int decode_header(const unsigned char header[H3_CHECKPOINT_HEADER_SIZE], + h3_checkpoint_info *info, uint64_t *hash, + char *error, size_t error_size) { + if (memcmp(header, h3_checkpoint_magic, sizeof(h3_checkpoint_magic)) || + get_u32(header + 8) != H3_CHECKPOINT_HEADER_SIZE) { + fail(error, error_size, "unsupported H3 checkpoint format"); + return 0; + } + uint32_t byte_order; + memcpy(&byte_order, header + 12, sizeof(byte_order)); + if (byte_order != H3_CHECKPOINT_ENDIAN) { + fail(error, error_size, "checkpoint byte order is unsupported"); + return 0; + } + memset(info, 0, sizeof(*info)); + info->signature = get_u64(header + 16); + info->seed = get_u64(header + 24); + info->total_steps = get_u32(header + 32); + info->next_step = get_u32(header + 36); + info->render_width = get_u32(header + 40); + info->render_height = get_u32(header + 44); + info->frame_count = get_u32(header + 48); + info->video_elements = get_u64(header + 56); + info->audio_elements = get_u64(header + 64); + info->denoise_seconds = bits_double(get_u64(header + 72)); + *hash = get_u64(header + H3_CHECKPOINT_HASH_OFFSET); + if (!valid_info(info)) { + fail(error, error_size, "checkpoint metadata is invalid"); + return 0; + } + return 1; +} + +static uint64_t checkpoint_hash( + const unsigned char header[H3_CHECKPOINT_HEADER_SIZE], + const h3_checkpoint_info *info, + const float *video, const float *audio) { + unsigned char canonical[H3_CHECKPOINT_HEADER_SIZE]; + memcpy(canonical, header, sizeof(canonical)); + memset(canonical + H3_CHECKPOINT_HASH_OFFSET, 0, sizeof(uint64_t)); + uint64_t hash = hash_bytes(UINT64_C(14695981039346656037), + canonical, sizeof(canonical)); + hash = hash_bytes(hash, video, + (size_t)info->video_elements * sizeof(*video)); + return hash_bytes(hash, audio, + (size_t)info->audio_elements * sizeof(*audio)); +} + +static int write_all(int descriptor, const void *data, size_t size) { + const unsigned char *bytes = data; + while (size) { + ssize_t written = write(descriptor, bytes, size); + if (written < 0 && errno == EINTR) continue; + if (written < 0) return 0; + if (written == 0) { + errno = EIO; + return 0; + } + bytes += (size_t)written; + size -= (size_t)written; + } + return 1; +} + +static int read_all(int descriptor, void *data, size_t size) { + unsigned char *bytes = data; + while (size) { + ssize_t count = read(descriptor, bytes, size); + if (count < 0 && errno == EINTR) continue; + if (count <= 0) return 0; + bytes += (size_t)count; + size -= (size_t)count; + } + return 1; +} + +int h3_checkpoint_save(const char *path, const h3_checkpoint_info *info, + const float *video, const float *audio, + char *error, size_t error_size) { + if (error && error_size) error[0] = '\0'; + if (!path || !*path || !video || !audio || !valid_info(info)) { + fail(error, error_size, "invalid checkpoint save arguments"); + return 0; + } + size_t path_length = strlen(path); + if (path_length > SIZE_MAX - sizeof(".tmp.XXXXXX")) { + fail(error, error_size, "checkpoint path is too long"); + return 0; + } + char *temporary = malloc(path_length + sizeof(".tmp.XXXXXX")); + if (!temporary) { + fail(error, error_size, "out of memory constructing checkpoint path"); + return 0; + } + snprintf(temporary, path_length + sizeof(".tmp.XXXXXX"), + "%s.tmp.XXXXXX", path); + int descriptor = mkstemp(temporary); + if (descriptor < 0) { + fail(error, error_size, "cannot create checkpoint %s: %s", + temporary, strerror(errno)); + free(temporary); + return 0; + } + if (fcntl(descriptor, F_SETFD, FD_CLOEXEC) != 0) { + int saved_errno = errno; + close(descriptor); + unlink(temporary); + fail(error, error_size, "cannot secure checkpoint %s: %s", + temporary, strerror(saved_errno)); + free(temporary); + return 0; + } + unsigned char header[H3_CHECKPOINT_HEADER_SIZE]; + encode_header(header, info, 0); + put_u64(header + H3_CHECKPOINT_HASH_OFFSET, + checkpoint_hash(header, info, video, audio)); + size_t video_bytes = (size_t)info->video_elements * sizeof(*video); + size_t audio_bytes = (size_t)info->audio_elements * sizeof(*audio); + int ok = write_all(descriptor, header, sizeof(header)) && + write_all(descriptor, video, video_bytes) && + write_all(descriptor, audio, audio_bytes) && + fsync(descriptor) == 0; + int saved_errno = errno; + if (close(descriptor) != 0 && ok) { + ok = 0; + saved_errno = errno; + } + /* The file fsync prevents a torn replacement. The rename is atomic, but + * this does not promise directory-entry durability across power loss. */ + if (ok && rename(temporary, path) != 0) { + ok = 0; + saved_errno = errno; + } + if (!ok) { + unlink(temporary); + fail(error, error_size, "cannot write checkpoint %s: %s", + path, strerror(saved_errno)); + } + free(temporary); + return ok; +} + +static int compatible(const h3_checkpoint_info *expected, + const h3_checkpoint_info *actual) { + return expected->signature == actual->signature && + expected->seed == actual->seed && + expected->total_steps == actual->total_steps && + expected->render_width == actual->render_width && + expected->render_height == actual->render_height && + expected->frame_count == actual->frame_count && + expected->video_elements == actual->video_elements && + expected->audio_elements == actual->audio_elements; +} + +int h3_checkpoint_load(const char *path, + const h3_checkpoint_info *expected, + h3_checkpoint_info *actual, + float **video, float **audio, + char *error, size_t error_size) { + if (error && error_size) error[0] = '\0'; + if (video) *video = NULL; + if (audio) *audio = NULL; + if (!path || !*path || !expected || !actual || !video || !audio) { + fail(error, error_size, "invalid checkpoint load arguments"); + return 0; + } + int flags = O_RDONLY; +#ifdef O_CLOEXEC + flags |= O_CLOEXEC; +#endif +#ifdef O_NOFOLLOW + flags |= O_NOFOLLOW; +#endif + int descriptor = open(path, flags); + if (descriptor < 0) { + fail(error, error_size, "cannot open checkpoint %s: %s", + path, strerror(errno)); + return 0; + } + struct stat status; + unsigned char header[H3_CHECKPOINT_HEADER_SIZE]; + uint64_t stored_hash = 0; + int ok = fstat(descriptor, &status) == 0 && S_ISREG(status.st_mode) && + read_all(descriptor, header, sizeof(header)) && + decode_header(header, actual, &stored_hash, error, error_size); + if (!ok && error && error_size && !*error) + fail(error, error_size, "cannot read checkpoint %s", path); + if (ok && !compatible(expected, actual)) { + fail(error, error_size, + "checkpoint does not match prompt, model, seed, shape, or schedule"); + ok = 0; + } + uint64_t video_bytes = ok ? actual->video_elements * sizeof(float) : 0; + uint64_t audio_bytes = ok ? actual->audio_elements * sizeof(float) : 0; + uint64_t expected_bytes = H3_CHECKPOINT_HEADER_SIZE; + if (ok && (video_bytes > UINT64_MAX - expected_bytes || + audio_bytes > UINT64_MAX - expected_bytes - video_bytes)) { + fail(error, error_size, "checkpoint size overflows"); + ok = 0; + } + if (ok) expected_bytes += video_bytes + audio_bytes; + if (ok && (status.st_size < 0 || + (uint64_t)status.st_size != expected_bytes)) { + fail(error, error_size, "checkpoint file size is inconsistent"); + ok = 0; + } + if (ok) { + *video = malloc((size_t)video_bytes); + *audio = malloc((size_t)audio_bytes); + if (!*video || !*audio) { + fail(error, error_size, "out of memory loading checkpoint latents"); + ok = 0; + } + } + if (ok && (!read_all(descriptor, *video, (size_t)video_bytes) || + !read_all(descriptor, *audio, (size_t)audio_bytes))) { + fail(error, error_size, "checkpoint payload is truncated"); + ok = 0; + } + if (ok && checkpoint_hash(header, actual, *video, *audio) != stored_hash) { + fail(error, error_size, "checkpoint checksum does not match"); + ok = 0; + } + if (close(descriptor) != 0 && ok) { + fail(error, error_size, "cannot close checkpoint %s: %s", + path, strerror(errno)); + ok = 0; + } + if (!ok) { + free(*video); + free(*audio); + *video = NULL; + *audio = NULL; + } + return ok; +} diff --git a/h3_checkpoint.h b/h3_checkpoint.h new file mode 100644 index 0000000..d2a49bc --- /dev/null +++ b/h3_checkpoint.h @@ -0,0 +1,38 @@ +#ifndef H3_CHECKPOINT_H +#define H3_CHECKPOINT_H + +#include +#include + +typedef struct { + uint64_t signature; + uint64_t seed; + uint32_t total_steps; + uint32_t next_step; + uint32_t render_width; + uint32_t render_height; + uint32_t frame_count; + uint64_t video_elements; + uint64_t audio_elements; + double denoise_seconds; +} h3_checkpoint_info; + +/* Stable, non-secret compatibility fingerprint for the canonical run key. */ +uint64_t h3_checkpoint_signature(const char *text); + +/* Checkpoints are written atomically with mode 0600 and contain raw F32 video + * and audio latents. The checksum detects corruption; it is not an + * authenticity mechanism for untrusted files. */ +int h3_checkpoint_save(const char *path, const h3_checkpoint_info *info, + const float *video, const float *audio, + char *error, size_t error_size); + +/* expected describes every immutable run field. next_step and + * denoise_seconds are returned from the file and ignored in expected. */ +int h3_checkpoint_load(const char *path, + const h3_checkpoint_info *expected, + h3_checkpoint_info *actual, + float **video, float **audio, + char *error, size_t error_size); + +#endif diff --git a/tests/test_h3.c b/tests/test_h3.c index 3f66fa3..bc62e27 100644 --- a/tests/test_h3.c +++ b/tests/test_h3.c @@ -1,14 +1,17 @@ +#include "h3_checkpoint.h" #include "h3_host.h" #include "h3_dit.h" #include "h3_metal.h" #include "h3_safetensors.h" #include "h3_terminal.h" +#include #include #include #include #include #include +#include #include static int tests_run; @@ -373,6 +376,166 @@ static void test_dit_row_conversions(void) { CHECK(memcmp(audio, unpacked, sizeof(audio)) == 0); } +static void test_checkpoint_roundtrip(void) { + char path[] = "/tmp/h3_checkpoint_XXXXXX"; + int descriptor = mkstemp(path); + CHECK(descriptor >= 0); + CHECK(close(descriptor) == 0); + CHECK(unlink(path) == 0); + + const float video[] = {1.0f, -2.0f, 3.5f, 0.0f, 9.25f}; + const float replacement[] = {8.0f, 7.0f, 6.0f, 5.0f, 4.0f}; + const float audio[] = {-0.5f, 4.0f, 7.0f}; + h3_checkpoint_info info = { + h3_checkpoint_signature("prompt-and-model-key"), 42, 10, 4, + 512, 512, 362, + sizeof(video) / sizeof(*video), sizeof(audio) / sizeof(*audio), 12.75 + }; + char error[256]; + CHECK(h3_checkpoint_signature("") == + UINT64_C(14695981039346656037)); + CHECK(info.signature != 0); + CHECK(info.signature != h3_checkpoint_signature("different-key")); + CHECK(h3_checkpoint_save(path, &info, video, audio, + error, sizeof(error))); + struct stat status; + CHECK(stat(path, &status) == 0); + CHECK((status.st_mode & 077) == 0); + + h3_checkpoint_info expected = info; + expected.next_step = 0; + expected.denoise_seconds = 0.0; + h3_checkpoint_info actual; + float *loaded_video = NULL; + float *loaded_audio = NULL; + CHECK(h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(actual.next_step == 4); + CHECK(actual.denoise_seconds == 12.75); + CHECK(memcmp(loaded_video, video, sizeof(video)) == 0); + CHECK(memcmp(loaded_audio, audio, sizeof(audio)) == 0); + free(loaded_video); + free(loaded_audio); + + /* Saving again atomically replaces the previous complete artifact. */ + info.next_step = 5; + info.denoise_seconds = 14.0; + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + CHECK(h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(actual.next_step == 5); + CHECK(actual.denoise_seconds == 14.0); + CHECK(memcmp(loaded_video, replacement, sizeof(replacement)) == 0); + free(loaded_video); + free(loaded_audio); + + expected.signature ^= UINT64_C(1); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "does not match") != NULL); + expected.signature = info.signature; + + expected.seed++; + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "does not match") != NULL); + expected.seed = info.seed; + + /* The checksum covers metadata as well as the latent payload. */ + descriptor = open(path, O_WRONLY); + CHECK(descriptor >= 0); + CHECK(lseek(descriptor, 36, SEEK_SET) == 36); + const unsigned char changed_step = 6; + CHECK(write(descriptor, &changed_step, 1) == 1); + CHECK(close(descriptor) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "checksum") != NULL); + + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + descriptor = open(path, O_WRONLY); + CHECK(descriptor >= 0); + CHECK(lseek(descriptor, -1, SEEK_END) >= 0); + const unsigned char corrupt = 0xff; + CHECK(write(descriptor, &corrupt, 1) == 1); + CHECK(close(descriptor) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "checksum") != NULL); + + /* Exact-size checks reject both incomplete and trailing payload bytes. */ + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + CHECK(stat(path, &status) == 0); + CHECK(truncate(path, status.st_size - 1) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "size is inconsistent") != NULL); + + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + descriptor = open(path, O_WRONLY | O_APPEND); + CHECK(descriptor >= 0); + CHECK(write(descriptor, &corrupt, 1) == 1); + CHECK(close(descriptor) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "size is inconsistent") != NULL); + + /* Reject format and metadata boundaries before allocating payloads. */ + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + descriptor = open(path, O_WRONLY); + CHECK(descriptor >= 0); + const unsigned char bad_magic = 'X'; + CHECK(write(descriptor, &bad_magic, 1) == 1); + CHECK(close(descriptor) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "unsupported") != NULL); + + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + descriptor = open(path, O_WRONLY); + CHECK(descriptor >= 0); + CHECK(lseek(descriptor, 36, SEEK_SET) == 36); + const unsigned char zero_step[4] = {0, 0, 0, 0}; + CHECK(write(descriptor, zero_step, sizeof(zero_step)) == + (ssize_t)sizeof(zero_step)); + CHECK(close(descriptor) == 0); + CHECK(!h3_checkpoint_load(path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "metadata is invalid") != NULL); + + /* O_NOFOLLOW keeps a valid artifact from being loaded through a symlink. */ + CHECK(h3_checkpoint_save(path, &info, replacement, audio, + error, sizeof(error))); + char link_path[] = "/tmp/h3_checkpoint_link_XXXXXX"; + descriptor = mkstemp(link_path); + CHECK(descriptor >= 0); + CHECK(close(descriptor) == 0); + CHECK(unlink(link_path) == 0); + CHECK(symlink(path, link_path) == 0); + CHECK(!h3_checkpoint_load(link_path, &expected, &actual, + &loaded_video, &loaded_audio, + error, sizeof(error))); + CHECK(strstr(error, "cannot open") != NULL); + CHECK(unlink(link_path) == 0); + CHECK(unlink(path) == 0); +} + static void test_metal_probe(void) { h3_device_info info; char error[256]; @@ -407,6 +570,7 @@ int main(void) { test_rng_and_solver(); test_rgb_resize(); test_dit_row_conversions(); + test_checkpoint_roundtrip(); test_metal_probe(); test_terminal_zoom(); printf("ok: %d checks\n", tests_run); From c33b7dd175bdfc158d5efdf12e115470651d527a Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Aug 2026 00:23:48 +0200 Subject: [PATCH 3/5] Project paused renders to sigma zero --- h3_dit.c | 138 ++++++++++++++++++++++++++++++++++++++---------- h3_dit.h | 10 ++++ tests/test_h3.c | 11 ++++ 3 files changed, 130 insertions(+), 29 deletions(-) diff --git a/h3_dit.c b/h3_dit.c index 76f11e1..5599b53 100644 --- a/h3_dit.c +++ b/h3_dit.c @@ -82,6 +82,7 @@ struct h3_dit { unsigned core_reuse_interval; unsigned core_forward_count; int core_residual_ready; + int last_velocity_step; unsigned active_block_count; uint8_t block_active[H3_DIT_BLOCKS]; h3_layout layout; @@ -1381,6 +1382,7 @@ static h3_dit *load_dit(const char *weight_directory, fail(error, error_size, "out of memory creating DiT model"); return NULL; } + dit->last_velocity_step = -1; dit->fused_mlp = getenv("H3_DISABLE_FUSED_MLP") == NULL; /* The released final heads are F32, but their inputs are already BF16. * Converting these small weights once selects the Iris-derived tiled @@ -2137,9 +2139,53 @@ int h3_dit_reset_run(h3_dit *dit, } dit->core_forward_count = 0; dit->core_residual_ready = 0; + dit->last_velocity_step = -1; return 1; } +static int read_output_velocity(h3_dit *dit, + float *video_velocity, + float *audio_velocity, + char *error, size_t error_size) { + size_t video_row_elements = (size_t)dit->video_rows * VIDEO_PATCH; + size_t audio_row_elements = (size_t)dit->audio_rows * AUDIO_CHANNELS; + uint16_t *video_out = malloc(video_row_elements * sizeof(*video_out)); + uint16_t *audio_out = malloc(audio_row_elements * sizeof(*audio_out)); + float *video_f32 = malloc(video_row_elements * sizeof(*video_f32)); + float *audio_f32 = malloc(audio_row_elements * sizeof(*audio_f32)); + if (!video_out || !audio_out || !video_f32 || !audio_f32) { + fail(error, error_size, "out of memory reading DiT velocity"); + free(video_out); free(audio_out); + free(video_f32); free(audio_f32); + return 0; + } + int ok = h3_gpu_tensor_read_bf16( + dit->video_output_bf16, video_out, video_row_elements) && + h3_gpu_tensor_read_bf16( + dit->audio_output_bf16, audio_out, audio_row_elements); + if (!ok) fail(error, error_size, "cannot read DiT output velocity"); + if (ok) { + for (size_t index = 0; index < video_row_elements; index++) { + uint32_t bits = (uint32_t)video_out[index] << 16; + memcpy(&video_f32[index], &bits, sizeof(bits)); + } + for (size_t index = 0; index < audio_row_elements; index++) { + uint32_t bits = (uint32_t)audio_out[index] << 16; + memcpy(&audio_f32[index], &bits, sizeof(bits)); + } + ok = h3_dit_unpatchify_video( + video_f32, VIDEO_CHANNELS, dit->latent_t, dit->latent_h, + dit->latent_w, video_velocity, h3_dit_video_elements(dit)) && + h3_dit_unpack_audio( + audio_f32, AUDIO_CHANNELS, dit->audio_t, audio_velocity, + h3_dit_audio_elements(dit)); + if (!ok) fail(error, error_size, "cannot unpack DiT output velocity"); + } + free(video_out); free(audio_out); + free(video_f32); free(audio_f32); + return ok; +} + int h3_dit_forward(h3_dit *dit, int step, const float *video_latent, const float *audio_latent, float *video_velocity, float *audio_velocity, @@ -2154,17 +2200,12 @@ int h3_dit_forward(h3_dit *dit, int step, size_t audio_row_elements = (size_t)dit->audio_rows * AUDIO_CHANNELS; float *video_rows = malloc(video_row_elements * sizeof(*video_rows)); float *audio_rows = malloc(audio_row_elements * sizeof(*audio_rows)); - uint16_t *video_out = malloc(video_row_elements * sizeof(*video_out)); - uint16_t *audio_out = malloc(audio_row_elements * sizeof(*audio_out)); - float *video_f32 = malloc(video_row_elements * sizeof(*video_f32)); - float *audio_f32 = malloc(audio_row_elements * sizeof(*audio_f32)); - if (!video_rows || !audio_rows || !video_out || !audio_out || - !video_f32 || !audio_f32) { + if (!video_rows || !audio_rows) { fail(error, error_size, "out of memory packing DiT latents"); - free(video_rows); free(audio_rows); free(video_out); free(audio_out); - free(video_f32); free(audio_f32); + free(video_rows); free(audio_rows); return 0; } + dit->last_velocity_step = -1; int ok = h3_dit_patchify_video(video_latent, VIDEO_CHANNELS, dit->latent_t, dit->latent_h, dit->latent_w, video_rows, video_row_elements) && @@ -2180,29 +2221,66 @@ int h3_dit_forward(h3_dit *dit, int step, audio_rows, audio_row_elements); if (!ok) fail(error, error_size, "cannot pack/write DiT input latents"); if (ok) ok = encode_forward(dit, step, 1, 1, 0, error, error_size); - if (ok) ok = h3_gpu_tensor_read_bf16(dit->video_output_bf16, video_out, - video_row_elements) && - h3_gpu_tensor_read_bf16(dit->audio_output_bf16, audio_out, - audio_row_elements); - if (!ok && (!error || !*error)) fail(error, error_size, "cannot read DiT output"); + if (ok) ok = read_output_velocity( + dit, video_velocity, audio_velocity, error, error_size); + if (ok) dit->last_velocity_step = step; + free(video_rows); free(audio_rows); + return ok; +} + +int h3_dit_project_draft_to_zero( + h3_dit *dit, int completed_steps, + const float *video_latent, size_t video_elements, + const float *audio_latent, size_t audio_elements, + float *video_draft, float *audio_draft, + char *error, size_t error_size) { + if (error && error_size) error[0] = '\0'; + if (!dit) { + fail(error, error_size, "invalid DiT draft projection arguments"); + return 0; + } + size_t expected_video = h3_dit_video_elements(dit); + size_t expected_audio = h3_dit_audio_elements(dit); + if (completed_steps <= 0 || + completed_steps >= dit->sigmas.steps || + dit->last_velocity_step != completed_steps - 1 || + !video_latent || video_elements != expected_video || + !audio_latent || audio_elements != expected_audio || + !video_draft || !audio_draft) { + fail(error, error_size, "invalid DiT draft projection arguments"); + return 0; + } + float *video_velocity = malloc(video_elements * sizeof(*video_velocity)); + float *audio_velocity = malloc(audio_elements * sizeof(*audio_velocity)); + if (!video_velocity || !audio_velocity) { + fail(error, error_size, "out of memory projecting DiT draft"); + free(video_velocity); + free(audio_velocity); + return 0; + } + int ok = read_output_velocity( + dit, video_velocity, audio_velocity, error, error_size); if (ok) { - for (size_t index = 0; index < video_row_elements; index++) { - uint32_t bits = (uint32_t)video_out[index] << 16; - memcpy(&video_f32[index], &bits, sizeof(bits)); - } - for (size_t index = 0; index < audio_row_elements; index++) { - uint32_t bits = (uint32_t)audio_out[index] << 16; - memcpy(&audio_f32[index], &bits, sizeof(bits)); - } + /* The stopped state is x_next = x + (sigma - sigma_next) * v. + * Extending that same data-ward velocity by sigma_next reaches the + * display-only zero-sigma estimate without mutating x_next. */ + if (video_draft != video_latent) + memcpy(video_draft, video_latent, + video_elements * sizeof(*video_draft)); + if (audio_draft != audio_latent) + memcpy(audio_draft, audio_latent, + audio_elements * sizeof(*audio_draft)); + ok = h3_euler_velocity_step( + video_draft, video_velocity, video_elements, + dit->sigmas.video[completed_steps], 0.0f) && + h3_euler_velocity_step( + audio_draft, audio_velocity, audio_elements, + dit->sigmas.audio[completed_steps], 0.0f); + if (!ok) fail(error, error_size, + "cannot project DiT draft to sigma zero"); } - if (ok) ok = h3_dit_unpatchify_video(video_f32, VIDEO_CHANNELS, - dit->latent_t, dit->latent_h, dit->latent_w, video_velocity, - h3_dit_video_elements(dit)) && - h3_dit_unpack_audio(audio_f32, AUDIO_CHANNELS, dit->audio_t, - audio_velocity, h3_dit_audio_elements(dit)); - if (!ok && (!error || !*error)) fail(error, error_size, "cannot unpack DiT output"); - free(video_rows); free(audio_rows); free(video_out); free(audio_out); - free(video_f32); free(audio_f32); + free(video_velocity); + free(audio_velocity); return ok; } @@ -2394,6 +2472,7 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, unsigned pending_evaluations = 0; int command_active = 0; int completed_steps = start_step; + dit->last_velocity_step = -1; for (int step = start_step; step < dit->sigmas.steps && ok; step++) { report(progress, progress_opaque, "denoise enqueue", step, dit->sigmas.steps); @@ -2421,6 +2500,7 @@ static int denoise_euler_gpu(h3_dit *dit, float *video_latent, error, error_size); if (ok) { last_evaluated = step; + dit->last_velocity_step = step; pending_evaluations++; } } diff --git a/h3_dit.h b/h3_dit.h index 4ed0950..6803e3c 100644 --- a/h3_dit.h +++ b/h3_dit.h @@ -102,6 +102,16 @@ int h3_dit_reset_run(h3_dit *dit, size_t h3_dit_video_elements(const h3_dit *dit); size_t h3_dit_audio_elements(const h3_dit *dit); +/* Build a display-only sigma-zero estimate from a stopped Euler state and the + * last computed data-ward velocity. The checkpoint latents are read-only and + * remain suitable for exact schedule continuation. */ +int h3_dit_project_draft_to_zero( + h3_dit *dit, int completed_steps, + const float *video_latent, size_t video_elements, + const float *audio_latent, size_t audio_elements, + float *video_draft, float *audio_draft, + char *error, size_t error_size); + /* One raw data-ward velocity evaluation. Input/output video layout is * [24,T,H,W], audio is [32,2,T], all F32 on the host boundary. */ int h3_dit_forward(h3_dit *dit, int step, diff --git a/tests/test_h3.c b/tests/test_h3.c index bc62e27..e611eb2 100644 --- a/tests/test_h3.c +++ b/tests/test_h3.c @@ -325,6 +325,17 @@ static void test_rng_and_solver(void) { float euler[] = {1.0f, 3.0f}; CHECK(h3_euler_velocity_step(euler, velocity, 2, 0.75f, 0.25f)); CHECK(euler[0] == 2.0f && euler[1] == 1.0f); + /* Projecting a copy of a stopped Euler state must equal extending the + * same velocity directly to zero, while the checkpoint stays unchanged. */ + float projected[] = {euler[0], euler[1]}; + float direct_to_zero[] = {1.0f, 3.0f}; + CHECK(h3_euler_velocity_step( + projected, velocity, 2, 0.25f, 0.0f)); + CHECK(h3_euler_velocity_step( + direct_to_zero, velocity, 2, 0.75f, 0.0f)); + CHECK(projected[0] == direct_to_zero[0]); + CHECK(projected[1] == direct_to_zero[1]); + CHECK(euler[0] == 2.0f && euler[1] == 1.0f); CHECK(!h3_euler_velocity_step(euler, velocity, 2, 0.25f, 0.25f)); } From 34d19b5776a9c37ddb81145ab82d669ad26ae9b2 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Aug 2026 00:28:28 +0200 Subject: [PATCH 4/5] Expose progressive checkpoints in the API and CLI --- h3.c | 214 ++++++++++++++++++++++++++++++++++++++++++++---- h3.h | 23 +++++- main.c | 57 +++++++++++++ tests/test_h3.c | 4 + 4 files changed, 280 insertions(+), 18 deletions(-) diff --git a/h3.c b/h3.c index 5c92176..5b8c984 100644 --- a/h3.c +++ b/h3.c @@ -1,5 +1,6 @@ #include "h3_internal.h" #include "h3_audio_vae.h" +#include "h3_checkpoint.h" #include "h3_host.h" #include "h3_dit.h" #include "h3_ffmpeg.h" @@ -190,6 +191,39 @@ static char *h3_prepared_key(const char *conditioning, return key.text; } +static uint64_t h3_checkpoint_run_signature( + const char *conditioning, const char *model_dir, + const h3_params *params, int render_width, int render_height) { + /* Compatibility describes sampler state, not the Metal implementation + * selected for its next forward pass. Normalize diagnostic kernels while + * keeping prompt, model, seed, shape, schedule, RoPE, and semantic quality + * controls strict. */ + h3_params normalized = *params; + normalized.use_slower_bf16_mlp = 0; + normalized.use_slower_bf16_qkv = 0; + normalized.use_slower_bf16_attention_output = 0; + normalized.use_slower_row_major_attention_output = 0; + normalized.use_slower_unfused_int8_inputs = 0; + normalized.use_slower_unfused_qkv_rope = 0; + normalized.use_slower_scalar_qkv_rms = 0; + normalized.use_slower_uncached_int8_scales = 0; + normalized.use_slower_dynamic_fc1_k = 0; + normalized.use_slower_grouped_quantizer = 0; + char *prepared = h3_prepared_key( + conditioning, &normalized, render_width, render_height); + if (!prepared) return 0; + h3_key key = {0}; + int ok = h3_key_append( + &key, + "checkpoint-schema=1|%s|model=%zu:%s|seed=%llu|denoise-reuse=%d", + prepared, strlen(model_dir), model_dir, + (unsigned long long)params->seed, params->denoise_reuse); + free(prepared); + uint64_t signature = ok ? h3_checkpoint_signature(key.text) : 0; + free(key.text); + return signature; +} + static int h3_text_embedding_copy(h3_text_embedding *destination, const h3_text_embedding *source) { memset(destination, 0, sizeof(*destination)); @@ -561,6 +595,45 @@ static int h3_valid_params(h3_ctx *ctx, const h3_params *params) { h3_set_error(ctx, "denoising preview requires a frame callback"); return 0; } + if (!isfinite(params->checkpoint_after_seconds) || + params->checkpoint_after_seconds < 0.0) { + h3_set_error(ctx, "checkpoint seconds must be finite and non-negative"); + return 0; + } + if (params->checkpoint_after_step < 0 || + params->checkpoint_after_step >= params->steps) { + h3_set_error(ctx, + "checkpoint step must be zero or smaller than denoising steps"); + return 0; + } + if (params->checkpoint_after_seconds > 0.0 && + params->checkpoint_after_step > 0) { + h3_set_error(ctx, + "checkpoint seconds and checkpoint step are mutually exclusive"); + return 0; + } + int checkpoint_requested = params->checkpoint_after_seconds > 0.0 || + params->checkpoint_after_step > 0; + if (params->checkpoint_path && !*params->checkpoint_path) { + h3_set_error(ctx, "checkpoint path must not be empty"); + return 0; + } + if (checkpoint_requested != + (params->checkpoint_path && *params->checkpoint_path)) { + h3_set_error(ctx, + "checkpoint threshold and checkpoint path must be set together"); + return 0; + } + if (params->resume_path && !*params->resume_path) { + h3_set_error(ctx, "resume checkpoint path must not be empty"); + return 0; + } + if ((checkpoint_requested || params->resume_path) && + (params->denoise_reuse != 1 || params->core_reuse != 1)) { + h3_set_error(ctx, + "progressive checkpointing currently requires reuse 1 and core reuse 1"); + return 0; + } if (params->core_reuse > 1 && params->denoise_reuse > 1) { h3_set_error(ctx, "core reuse and denoiser reuse cannot be combined"); return 0; @@ -862,6 +935,9 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, h3_temporal_shape temporal = h3_temporal(params->frames); int latent_w, latent_h; h3_latent_canvas(render_width, render_height, &latent_w, &latent_h); + size_t video_count = (size_t)24 * (size_t)temporal.video_t * + (size_t)latent_h * (size_t)latent_w; + size_t audio_count = (size_t)32 * 2 * (size_t)temporal.audio_t; h3_tokenizer *tokenizer = NULL; uint32_t *ids = NULL; size_t token_count = 0; @@ -895,6 +971,7 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, h3_live_preview live_preview; memset(&live_preview, 0, sizeof(live_preview)); float *video = NULL, *audio = NULL; + float *draft_video = NULL, *draft_audio = NULL; h3_video_frames frames; memset(&frames, 0, sizeof(frames)); h3_audio_waveform waveform; @@ -908,6 +985,12 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, int conditioned = 0; int dit_is_cached = 0; int decoder_is_cached = 0; + uint64_t checkpoint_signature = 0; + int resumed_from_step = 0; + int checkpointed = 0; + double prior_denoise_seconds = 0.0; + h3_dit_progressive progressive = {0}; + char detail[512]; char *tokenizer_path = h3_path(ctx->model_dir, ref2va ? "Ref2VA/tokenizer/tokenizer.json" : "FL2VA/tokenizer/tokenizer.json"); char *text_path = h3_path(ctx->model_dir, ref2va ? @@ -935,6 +1018,45 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, h3_set_error(ctx, "out of memory constructing prepared-model cache key"); goto cleanup; } + int progressive_requested = params->resume_path || + params->checkpoint_after_seconds > 0.0 || + params->checkpoint_after_step > 0; + if (progressive_requested) { + checkpoint_signature = h3_checkpoint_run_signature( + conditioning_key, ctx->model_dir, params, + render_width, render_height); + if (!checkpoint_signature) { + h3_set_error(ctx, "cannot construct checkpoint signature"); + goto cleanup; + } + } + if (params->resume_path) { + h3_checkpoint_info expected = { + checkpoint_signature, params->seed, (uint32_t)params->steps, 0, + (uint32_t)render_width, (uint32_t)render_height, + (uint32_t)temporal.frame_count, + (uint64_t)video_count, (uint64_t)audio_count, 0.0 + }; + h3_checkpoint_info actual; + h3_progress_emit(&progress, "checkpoint load", 0, 1); + if (progress.cancelled) goto cleanup; + if (!h3_checkpoint_load( + params->resume_path, &expected, &actual, &video, &audio, + detail, sizeof(detail))) { + h3_set_error(ctx, "%s", detail); + goto cleanup; + } + resumed_from_step = (int)actual.next_step; + prior_denoise_seconds = actual.denoise_seconds; + if (params->checkpoint_after_step > 0 && + params->checkpoint_after_step <= resumed_from_step) { + h3_set_error(ctx, + "checkpoint step must be greater than the resumed step"); + goto cleanup; + } + h3_progress_emit(&progress, "checkpoint load", 1, 1); + if (progress.cancelled) goto cleanup; + } h3_key decoder_cache_key = {0}; if (!h3_key_append(&decoder_cache_key, "%s|%dx%d", vae_path, latent_h, latent_w)) { @@ -958,7 +1080,6 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, } conditioning_hit = ctx->cache_enabled && ctx->conditioning_key && !strcmp(ctx->conditioning_key, conditioning_key); - char detail[512]; if (conditioning_hit) { size_t cached_reference_count = 0; if (!h3_conditioning_cache_load( @@ -1546,24 +1667,33 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, live_preview.output_height = params->height; if (progress.cancelled) goto cleanup; } - size_t video_count = h3_dit_video_elements(dit); - size_t audio_count = h3_dit_audio_elements(dit); - video = malloc(video_count * sizeof(*video)); - audio = malloc(audio_count * sizeof(*audio)); - if (!video || !audio) { - h3_set_error(ctx, "out of memory allocating joint H3 noise"); + if (h3_dit_video_elements(dit) != video_count || + h3_dit_audio_elements(dit) != audio_count) { + h3_set_error(ctx, "prepared DiT latent shape is inconsistent"); goto cleanup; } - /* The released server initializes each modality from a separate generator - * carrying the same requested seed. */ - h3_rng video_rng, audio_rng; - h3_rng_seed(&video_rng, params->seed); - h3_rng_seed(&audio_rng, params->seed); - h3_rng_fill_normal(&video_rng, video, video_count); - h3_rng_fill_normal(&audio_rng, audio, audio_count); - if (!h3_dit_denoise_euler_preview( + if (!params->resume_path) { + video = malloc(video_count * sizeof(*video)); + audio = malloc(audio_count * sizeof(*audio)); + if (!video || !audio) { + h3_set_error(ctx, "out of memory allocating joint H3 noise"); + goto cleanup; + } + /* The released server initializes each modality from a separate + * generator carrying the same requested seed. */ + h3_rng video_rng, audio_rng; + h3_rng_seed(&video_rng, params->seed); + h3_rng_seed(&audio_rng, params->seed); + h3_rng_fill_normal(&video_rng, video, video_count); + h3_rng_fill_normal(&audio_rng, audio, audio_count); + } + progressive.start_step = resumed_from_step; + progressive.stop_after_seconds = params->checkpoint_after_seconds; + progressive.stop_after_step = params->checkpoint_after_step; + if (!h3_dit_denoise_euler_progressive( dit, video, audio, params->denoise_reuse, h3_dit_progress_bridge, &progress, + &progressive, preview_decoder ? h3_deliver_denoise_preview : NULL, preview_decoder ? &live_preview : NULL, detail, sizeof(detail))) { @@ -1576,9 +1706,52 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, } goto cleanup; } + if (progress.cancelled) goto cleanup; + if (progressive.stopped) { + h3_checkpoint_info info = { + checkpoint_signature, params->seed, (uint32_t)params->steps, + (uint32_t)progressive.completed_steps, + (uint32_t)render_width, (uint32_t)render_height, + (uint32_t)temporal.frame_count, + (uint64_t)video_count, (uint64_t)audio_count, + prior_denoise_seconds + progressive.elapsed_seconds + }; + h3_progress_emit(&progress, "checkpoint save", 0, 1); + if (progress.cancelled) goto cleanup; + if (!h3_checkpoint_save(params->checkpoint_path, &info, video, audio, + detail, sizeof(detail))) { + h3_set_error(ctx, "%s", detail); + goto cleanup; + } + checkpointed = 1; + h3_progress_emit(&progress, "checkpoint save", 1, 1); + if (progress.cancelled) goto cleanup; + draft_video = malloc(video_count * sizeof(*draft_video)); + draft_audio = malloc(audio_count * sizeof(*draft_audio)); + if (!draft_video || !draft_audio) { + h3_set_error(ctx, "out of memory allocating projected draft"); + goto cleanup; + } + h3_progress_emit(&progress, "draft projection", 0, 1); + if (progress.cancelled) goto cleanup; + if (!h3_dit_project_draft_to_zero( + dit, progressive.completed_steps, + video, video_count, audio, audio_count, + draft_video, draft_audio, detail, sizeof(detail))) { + h3_set_error(ctx, "%s", detail); + goto cleanup; + } + free(video); + free(audio); + video = draft_video; + audio = draft_audio; + draft_video = NULL; + draft_audio = NULL; + h3_progress_emit(&progress, "draft projection", 1, 1); + if (progress.cancelled) goto cleanup; + } if (!dit_is_cached) h3_dit_free(dit); dit = NULL; - if (progress.cancelled) goto cleanup; h3_progress_emit(&progress, "audio VAE", 0, 7); if (!h3_audio_vae_decode(audio_vae_path, "h3_shaders.metal", audio, temporal.audio_t, h3_audio_vae_progress_bridge, @@ -1676,6 +1849,13 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, result->fps = H3_FPS; result->sample_rate = waveform.sample_rate; result->seed = params->seed; + result->denoise_steps_completed = progressive.completed_steps; + result->denoise_steps_total = params->steps; + result->checkpointed = checkpointed; + result->resumed_from_step = resumed_from_step; + result->denoise_seconds = progressive.elapsed_seconds; + result->cumulative_denoise_seconds = + prior_denoise_seconds + progressive.elapsed_seconds; cleanup: free(conditioning_key); @@ -1708,7 +1888,7 @@ h3_result *h3_generate(h3_ctx *ctx, const char *prompt, h3_layout_free(&layout); if (!dit_is_cached) h3_dit_free(dit); if (!decoder_is_cached) h3_video_vae_decoder_free(preview_decoder); - free(video); free(audio); free(rgb8); + free(video); free(audio); free(draft_video); free(draft_audio); free(rgb8); h3_video_frames_free(&frames); h3_audio_waveform_free(&waveform); return result; diff --git a/h3.h b/h3.h index c3d3a9d..8a7264a 100644 --- a/h3.h +++ b/h3.h @@ -120,6 +120,17 @@ typedef struct { int use_slower_grouped_quantizer; /* Decode and deliver one representative frame after every Euler step. */ int preview_denoise; + /* Pause after the first completed denoising transition whose elapsed wall + * time reaches this threshold, save both modality latents, and decode a + * display-only sigma-zero projection as the draft. Zero disables it. */ + double checkpoint_after_seconds; + /* Pause immediately after this exact number of completed denoising + * transitions. Must be smaller than steps. Zero disables it. */ + int checkpoint_after_step; + const char *checkpoint_path; + /* Restore a compatible checkpoint and continue at its next transition. + * The original prompt and generation options remain required. */ + const char *resume_path; h3_frame_callback on_frame; h3_progress_callback on_progress; void *callback_opaque; @@ -128,7 +139,11 @@ typedef struct { #define H3_PARAMS_DEFAULT { \ H3_DEFAULT_WIDTH, H3_DEFAULT_HEIGHT, H3_DEFAULT_FRAMES, H3_DEFAULT_STEPS, \ UINT64_C(42), NULL, NULL, NULL, NULL, 0, H3_REFERENCE_IMAGE_MATCH, \ - 1, H3_DEFAULT_DIT_LAYERS, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, NULL, NULL \ + 1, H3_DEFAULT_DIT_LAYERS, 1, \ + 0, 0, 0, 0, 0, \ + 0, 0, 0, 0, 0, \ + 0, 0, 0, 0, 0, 0, \ + 0.0, 0, NULL, NULL, NULL, NULL, NULL \ } typedef struct { @@ -164,6 +179,12 @@ struct h3_result { int fps; int sample_rate; uint64_t seed; + int denoise_steps_completed; + int denoise_steps_total; + int checkpointed; + int resumed_from_step; + double denoise_seconds; + double cumulative_denoise_seconds; }; /* Load model metadata and initialize the Metal device. Weights remain unmapped. */ diff --git a/main.c b/main.c index a694b37..dae6f62 100644 --- a/main.c +++ b/main.c @@ -55,6 +55,10 @@ static void usage(const char *program) { " --ref-audio PATH Append an ordered standalone audio clip\n" " --frames-dir PATH Write generated frames as PPM files\n" " --show Display a frame after every denoising step (M5)\n" + " --checkpoint-after-seconds N Pause after denoise wall-time threshold\n" + " --checkpoint-after-step N Pause after exactly N denoise steps\n" + " --checkpoint PATH Save latents and decode sigma-zero draft\n" + " --resume PATH Continue a compatible progressive checkpoint\n" " --zoom N Terminal image zoom (default: 2 for Retina)\n" " --profile Print per-phase Metal timing and allocation data\n" " --info Inspect model/device without mapping weights\n" @@ -91,6 +95,17 @@ static int frames_from_seconds(const char *value) { return (int)rounded; } +static double parse_positive_seconds(const char *value, const char *label) { + char *end = NULL; + errno = 0; + double seconds = strtod(value, &end); + if (errno || !end || *end || !isfinite(seconds) || seconds <= 0.0) { + fprintf(stderr, "h3: invalid %s: %s\n", label, value); + exit(2); + } + return seconds; +} + static uint64_t parse_u64(const char *value, const char *label) { char *end = NULL; errno = 0; @@ -249,6 +264,8 @@ int main(int argc, char **argv) { OPT_FIRST, OPT_LAST, OPT_REF_IMAGE, OPT_REF_IMAGE_SIZE, OPT_REF_VIDEO, OPT_REF_SILENT_VIDEO, OPT_REF_VIDEO_AUDIO, OPT_REF_AUDIO, OPT_FRAMES_DIR, OPT_SHOW, OPT_ZOOM, + OPT_CHECKPOINT_AFTER_SECONDS, OPT_CHECKPOINT_AFTER_STEP, + OPT_CHECKPOINT, OPT_RESUME, OPT_PROFILE, OPT_INFO }; static const struct option options[] = { {"model-dir", required_argument, NULL, 'd'}, @@ -298,6 +315,12 @@ int main(int argc, char **argv) { {"ref-audio", required_argument, NULL, OPT_REF_AUDIO}, {"frames-dir", required_argument, NULL, OPT_FRAMES_DIR}, {"show", no_argument, NULL, OPT_SHOW}, + {"checkpoint-after-seconds", required_argument, NULL, + OPT_CHECKPOINT_AFTER_SECONDS}, + {"checkpoint-after-step", required_argument, NULL, + OPT_CHECKPOINT_AFTER_STEP}, + {"checkpoint", required_argument, NULL, OPT_CHECKPOINT}, + {"resume", required_argument, NULL, OPT_RESUME}, {"zoom", required_argument, NULL, OPT_ZOOM}, {"profile", no_argument, NULL, OPT_PROFILE}, {"info", no_argument, NULL, OPT_INFO}, @@ -449,6 +472,16 @@ int main(int argc, char **argv) { } case OPT_FRAMES_DIR: cli.frames_dir = optarg; break; case OPT_SHOW: show = 1; break; + case OPT_CHECKPOINT_AFTER_SECONDS: + params.checkpoint_after_seconds = parse_positive_seconds( + optarg, "checkpoint seconds"); + break; + case OPT_CHECKPOINT_AFTER_STEP: + params.checkpoint_after_step = parse_int( + optarg, "checkpoint step"); + break; + case OPT_CHECKPOINT: params.checkpoint_path = optarg; break; + case OPT_RESUME: params.resume_path = optarg; break; case OPT_ZOOM: if (!h3_terminal_set_zoom(parse_int(optarg, "zoom"))) { fprintf(stderr, "h3: --zoom must be at least 1\n"); @@ -468,6 +501,12 @@ int main(int argc, char **argv) { fprintf(stderr, "h3: --seconds and --frames are mutually exclusive\n"); return 2; } + if (!prompt && (params.checkpoint_after_seconds > 0.0 || + params.checkpoint_after_step > 0 || + params.checkpoint_path || params.resume_path)) { + fprintf(stderr, "h3: checkpoint options require --prompt\n"); + return 2; + } if (prompt && params.steps >= 2 && params.steps <= 7 && params.denoise_reuse > 1) { fprintf(stderr, @@ -513,6 +552,24 @@ int main(int argc, char **argv) { h3_free(ctx); return 1; } + if (result->checkpointed) { + fprintf(stderr, + "h3: sigma-zero draft checkpointed at step %d/%d; %.2f " + "denoise seconds this session (%.2f cumulative) -> %s\n", + result->denoise_steps_completed, result->denoise_steps_total, + result->denoise_seconds, + result->cumulative_denoise_seconds, params.checkpoint_path); + } else if (result->resumed_from_step > 0) { + fprintf(stderr, + "h3: resumed at step %d/%d and completed step %d/%d\n", + result->resumed_from_step, result->denoise_steps_total, + result->denoise_steps_completed, result->denoise_steps_total); + } else if (params.checkpoint_after_seconds > 0.0 || + params.checkpoint_after_step > 0) { + fprintf(stderr, + "h3: denoising completed before the checkpoint threshold; " + "no checkpoint was written\n"); + } h3_result_free(result); if (output && *output) fprintf(stderr, "h3: wrote %s\n", output); if (cli.frames_dir) diff --git a/tests/test_h3.c b/tests/test_h3.c index e611eb2..830987f 100644 --- a/tests/test_h3.c +++ b/tests/test_h3.c @@ -75,6 +75,10 @@ static void test_schedule(void) { h3_params defaults = H3_PARAMS_DEFAULT; CHECK(defaults.steps == 20); CHECK(defaults.use_reference_rope == 0); + CHECK(defaults.checkpoint_after_seconds == 0.0); + CHECK(defaults.checkpoint_after_step == 0); + CHECK(defaults.checkpoint_path == NULL); + CHECK(defaults.resume_path == NULL); h3_sigma_schedule schedule; CHECK(h3_schedule_build(20, &schedule)); From 2f913be3d33951fb2e9fccec73479ced6f7190b3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Aug 2026 00:29:06 +0200 Subject: [PATCH 5/5] Document progressive draft rendering --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/README.md b/README.md index 090acbe..08a12b6 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,84 @@ against a 29-pass reference; an independent surfer test measured 0.547. The four-pass denoise took about 3.5 seconds on M5 Max, versus 26.4 seconds for the reference. +#### Pause a long render and inspect a draft + +Progressive checkpoints let a final denoising schedule stop at a safe Euler +transition. H3 saves the untouched video and audio sampler state, then projects +a separate copy to sigma zero and decodes it as a normal draft MP4. The +projection reuses the velocity from the completed transition; it does not run +an extra DiT pass. + +Choose the final step budget at the start. This example plans a 20-step render +but pauses after step 10: + +```sh +PROMPT='A red fox walks through fresh snow in a pine forest. Medium tracking shot, natural winter light, realistic fur, soft footsteps and wind.' + +./h3 --profile \ + -d ./MiniMax-H3 -p "$PROMPT" \ + --width 512 --height 512 --frames 22 \ + --steps 20 --layers 50 --reuse 1 --core-reuse 1 \ + --checkpoint-after-step 10 \ + --checkpoint outputs/fox-progress.h3ckpt \ + -o outputs/fox-draft.mp4 +``` + +If the direction looks right, continue the same schedule instead of starting +again: + +```sh +./h3 --profile \ + -d ./MiniMax-H3 -p "$PROMPT" \ + --width 512 --height 512 --frames 22 \ + --steps 20 --layers 50 --reuse 1 --core-reuse 1 \ + --resume outputs/fox-progress.h3ckpt \ + -o outputs/fox-final.mp4 +``` + +To inspect another intermediate result, resume with a later absolute step and +write a new checkpoint: + +```sh +./h3 --profile \ + -d ./MiniMax-H3 -p "$PROMPT" \ + --width 512 --height 512 --frames 22 \ + --steps 20 --layers 50 --reuse 1 --core-reuse 1 \ + --resume outputs/fox-progress.h3ckpt \ + --checkpoint-after-step 15 \ + --checkpoint outputs/fox-progress-15.h3ckpt \ + -o outputs/fox-draft-15.mp4 +``` + +`--checkpoint-after-step N` stops after exactly N completed transitions. +`--checkpoint-after-seconds N` instead stops at the first completed transition +whose denoising wall time reaches the threshold. A running Metal command cannot +be suspended midway through a transition. The threshold excludes model loading, +conditioning, draft VAE decoding, and FFmpeg work. On resume, a step threshold +remains absolute within the original schedule, while a seconds threshold starts +again for the current invocation. H3 reports both session and cumulative +denoising time when it writes another checkpoint. + +The two threshold options are mutually exclusive. If denoising reaches its +normal end first, H3 writes the final output and no checkpoint. Draft decoding +still pays the audio/video VAE and FFmpeg cost; the saved work is the remaining +DiT forwards. Early sigma-zero projections are estimates, so detail and anatomy +become more reliable at later stops. + +Resume requires the same prompt, model directory, seed, denoising schedule, +internal render shape, references, and semantic quality controls. Progressive +v1 also requires `--reuse 1 --core-reuse 1`; accelerated reuse modes carry +history that is not stored in the checkpoint. Checkpoints are private mode-0600 +files containing raw F32 latents and a corruption checksum. The checksum is not +authentication, so do not load checkpoints from untrusted sources. Keep the +same `-d` path spelling and do not modify or retimestamp reference files between +pause and resume. + +A completed low-step run cannot be extended into a different higher-step +schedule because its last transition has already reached sigma zero. To inspect +roughly five of twenty passes, start with `--steps 20` and checkpoint that +schedule after step 5. + ### 3. Move toward reference quality Change one control at a time when evaluating quality. First restore all layers,