mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-11-11 11:26:34 +00:00
feat(server): support cancellation of running generation jobs (#2190)
This commit is contained in:
1 parent
deaeed6a2f
commit
e779d4f149
5 files changed
+78
-21
No files matched your search
@@ -1037,7 +1037,11 @@ Notes:
|
|||||||
|
|
||||||
#### `POST /sdcpp/v1/jobs/{id}/cancel`
|
#### `POST /sdcpp/v1/jobs/{id}/cancel`
|
||||||
|
|
||||||
Attempts to cancel an accepted job.
|
Cancels a queued job immediately, or requests interruption of a generating image or
|
||||||
|
video job. A generating job returns `200 OK` with `cancel_requested: true` and
|
||||||
|
remains `generating` until execution stops at a cancellation checkpoint. Continue
|
||||||
|
polling until its status becomes `cancelled`. Cancelled jobs have no result.
|
||||||
|
Repeated cancellation requests are safe; terminal jobs are returned unchanged.
|
||||||
|
|
||||||
Typical status codes:
|
Typical status codes:
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,46 @@
|
|||||||
#include "async_jobs.h"
|
#include "async_jobs.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
#include "common/media_io.h"
|
#include "common/media_io.h"
|
||||||
#include "common/resource_owners.hpp"
|
#include "common/resource_owners.hpp"
|
||||||
|
|
||||||
|
class JobCancellationMonitor {
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
bool done_ = false;
|
||||||
|
std::thread thread_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// The caller holds sd_ctx_mutex until this monitor has joined, keeping the
|
||||||
|
// context alive and preventing cancellation from reaching another request.
|
||||||
|
JobCancellationMonitor(sd_ctx_t* ctx, const AsyncGenerationJob& job)
|
||||||
|
: thread_([this, ctx, &job]() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex_);
|
||||||
|
while (!done_) {
|
||||||
|
if (job.cancel_requested.load()) {
|
||||||
|
// Each generation pass resets the library flag, including ADetailer.
|
||||||
|
sd_cancel_generation(ctx, SD_CANCEL_ALL);
|
||||||
|
}
|
||||||
|
cv_.wait_for(lock, std::chrono::milliseconds(10), [this]() { return done_; });
|
||||||
|
}
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
~JobCancellationMonitor() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
done_ = true;
|
||||||
|
}
|
||||||
|
cv_.notify_one();
|
||||||
|
thread_.join();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) {
|
static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) {
|
||||||
(void)is_noisy;
|
(void)is_noisy;
|
||||||
if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) {
|
if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) {
|
||||||
@@ -142,6 +175,7 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager.queue.erase(new_end, manager.queue.end());
|
manager.queue.erase(new_end, manager.queue.end());
|
||||||
|
job.cancel_requested.store(true);
|
||||||
job.status = AsyncJobStatus::Cancelled;
|
job.status = AsyncJobStatus::Cancelled;
|
||||||
job.completed_at = unix_timestamp_now_ms();
|
job.completed_at = unix_timestamp_now_ms();
|
||||||
job.result_images_b64.clear();
|
job.result_images_b64.clear();
|
||||||
@@ -156,13 +190,14 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
|
|||||||
|
|
||||||
json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJob& job) {
|
json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJob& job) {
|
||||||
json result;
|
json result;
|
||||||
result["id"] = job.id;
|
result["id"] = job.id;
|
||||||
result["kind"] = async_job_kind_name(job.kind);
|
result["kind"] = async_job_kind_name(job.kind);
|
||||||
result["status"] = async_job_status_name(job.status);
|
result["status"] = async_job_status_name(job.status);
|
||||||
result["created"] = job.created_at;
|
result["cancel_requested"] = job.cancel_requested.load();
|
||||||
result["started"] = job.started_at == 0 ? json(nullptr) : json(job.started_at);
|
result["created"] = job.created_at;
|
||||||
result["completed"] = job.completed_at == 0 ? json(nullptr) : json(job.completed_at);
|
result["started"] = job.started_at == 0 ? json(nullptr) : json(job.started_at);
|
||||||
result["queue_position"] = 0;
|
result["completed"] = job.completed_at == 0 ? json(nullptr) : json(job.completed_at);
|
||||||
|
result["queue_position"] = 0;
|
||||||
|
|
||||||
if (job.status == AsyncJobStatus::Queued) {
|
if (job.status == AsyncJobStatus::Queued) {
|
||||||
size_t position = 1;
|
size_t position = 1;
|
||||||
@@ -235,6 +270,10 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
|||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||||
|
if (job.cancel_requested.load()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
|
||||||
set_preview_callback(job);
|
set_preview_callback(job);
|
||||||
sd_image_t* raw_results = nullptr;
|
sd_image_t* raw_results = nullptr;
|
||||||
int num_results = 0;
|
int num_results = 0;
|
||||||
@@ -244,6 +283,9 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
|||||||
}
|
}
|
||||||
clear_preview_callback();
|
clear_preview_callback();
|
||||||
results.adopt(raw_results, num_results);
|
results.adopt(raw_results, num_results);
|
||||||
|
if (job.cancel_requested.load()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!results.empty() && !apply_server_adetailer(runtime, job.img_gen, params, results)) {
|
if (!results.empty() && !apply_server_adetailer(runtime, job.img_gen, params, results)) {
|
||||||
error_message = "ADetailer failed";
|
error_message = "ADetailer failed";
|
||||||
return false;
|
return false;
|
||||||
@@ -251,7 +293,7 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int num_results = results.count();
|
const int num_results = results.count();
|
||||||
if (num_results <= 0) {
|
if (job.cancel_requested.load() || num_results <= 0) {
|
||||||
error_message = "generate_image returned no results";
|
error_message = "generate_image returned no results";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -314,6 +356,10 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
|
|||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||||
|
if (job.cancel_requested.load()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
|
||||||
set_preview_callback(job);
|
set_preview_callback(job);
|
||||||
sd_image_t* raw_results = nullptr;
|
sd_image_t* raw_results = nullptr;
|
||||||
if (!generate_video(runtime.sd_ctx, ¶ms, &raw_results, &num_results, &generated_audio, &output_fps)) {
|
if (!generate_video(runtime.sd_ctx, ¶ms, &raw_results, &num_results, &generated_audio, &output_fps)) {
|
||||||
@@ -324,7 +370,7 @@ bool execute_vid_gen_job(ServerRuntime& runtime,
|
|||||||
}
|
}
|
||||||
|
|
||||||
num_results = results.count();
|
num_results = results.count();
|
||||||
if (num_results <= 0) {
|
if (job.cancel_requested.load() || num_results <= 0) {
|
||||||
free_sd_audio(generated_audio);
|
free_sd_audio(generated_audio);
|
||||||
error_message = "generate_video returned no results";
|
error_message = "generate_video returned no results";
|
||||||
return false;
|
return false;
|
||||||
@@ -410,7 +456,11 @@ void async_job_worker(ServerRuntime& runtime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
job->completed_at = unix_timestamp_now_ms();
|
job->completed_at = unix_timestamp_now_ms();
|
||||||
if (ok) {
|
if (job->cancel_requested.load()) {
|
||||||
|
job->status = AsyncJobStatus::Cancelled;
|
||||||
|
job->error_code = "cancelled";
|
||||||
|
job->error_message = "job cancelled by client";
|
||||||
|
} else if (ok) {
|
||||||
job->status = AsyncJobStatus::Completed;
|
job->status = AsyncJobStatus::Completed;
|
||||||
job->result_images_b64 = std::move(output_images);
|
job->result_images_b64 = std::move(output_images);
|
||||||
job->result_media_b64 = std::move(output_media_b64);
|
job->result_media_b64 = std::move(output_media_b64);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
@@ -32,9 +33,10 @@ struct AsyncGenerationJob {
|
|||||||
std::string id;
|
std::string id;
|
||||||
AsyncJobKind kind = AsyncJobKind::ImgGen;
|
AsyncJobKind kind = AsyncJobKind::ImgGen;
|
||||||
AsyncJobStatus status = AsyncJobStatus::Queued;
|
AsyncJobStatus status = AsyncJobStatus::Queued;
|
||||||
int64_t created_at = unix_timestamp_now_ms();
|
std::atomic<bool> cancel_requested{false};
|
||||||
int64_t started_at = 0;
|
int64_t created_at = unix_timestamp_now_ms();
|
||||||
int64_t completed_at = 0;
|
int64_t started_at = 0;
|
||||||
|
int64_t completed_at = 0;
|
||||||
ImgGenJobRequest img_gen;
|
ImgGenJobRequest img_gen;
|
||||||
VidGenJobRequest vid_gen;
|
VidGenJobRequest vid_gen;
|
||||||
std::vector<std::string> result_images_b64;
|
std::vector<std::string> result_images_b64;
|
||||||
|
|||||||
+1
-1
Submodule examples/server/frontend updated: e36e34a2e2...34e5ad6edc.
@@ -206,7 +206,7 @@ static json make_img_gen_features_json() {
|
|||||||
{"hires", true},
|
{"hires", true},
|
||||||
{"cache", true},
|
{"cache", true},
|
||||||
{"cancel_queued", true},
|
{"cancel_queued", true},
|
||||||
{"cancel_generating", false},
|
{"cancel_generating", true},
|
||||||
{"preview", true},
|
{"preview", true},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ static json make_vid_gen_features_json() {
|
|||||||
{"vae_tiling", true},
|
{"vae_tiling", true},
|
||||||
{"cache", true},
|
{"cache", true},
|
||||||
{"cancel_queued", true},
|
{"cancel_queued", true},
|
||||||
{"cancel_generating", false},
|
{"cancel_generating", true},
|
||||||
{"preview", true},
|
{"preview", true},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -380,8 +380,8 @@ static json make_capabilities_json(ServerRuntime& runtime) {
|
|||||||
json top_level_defaults = json::object();
|
json top_level_defaults = json::object();
|
||||||
json top_level_output_formats = json::array();
|
json top_level_output_formats = json::array();
|
||||||
json top_level_features = {
|
json top_level_features = {
|
||||||
{"cancel_queued", true},
|
{"cancel_queued", true},
|
||||||
{"cancel_generating", false},
|
{"cancel_generating", true},
|
||||||
};
|
};
|
||||||
std::string current_mode = "";
|
std::string current_mode = "";
|
||||||
if (supports_img) {
|
if (supports_img) {
|
||||||
@@ -840,8 +840,9 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (job.status == AsyncJobStatus::Generating) {
|
if (job.status == AsyncJobStatus::Generating) {
|
||||||
res.status = 409;
|
job.cancel_requested.store(true);
|
||||||
res.set_content(R"({"error":"job is currently generating and cannot be interrupted yet"})", "application/json");
|
res.status = 200;
|
||||||
|
res.set_content(make_async_job_json(manager, job).dump(), "application/json");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user