refactor: enable CUDA graph replay for extensions

This commit is contained in:
leejet committed 2026-10-31 21:52:28 +08:00
1 parent ba2188624e
commit d8964d0302
8 files changed
+54 -18

No files matched your search

+41 -5
View File
@@ -236,11 +236,47 @@ ConvRot extension leaves that chain together to preserve its existing fusion.
Standalone builtin ConvRot and its native GEMM integration are still pending Standalone builtin ConvRot and its native GEMM integration are still pending
migration. migration.
CUDA extension kernels currently execute between native graph fragments. Native CUDA kernels can opt into device replay with `ggml_ext_register_kernel_ex` and
fragments retain CUDA Graph caching, but the extension execution plan is not `GGML_EXT_KERNEL_CAPTURE_SAFE`. The original registration function retains its
captured as a whole. Moving small fusions out of native fragments can therefore per-execution callback behavior, and the descriptor ABI is unchanged. Opting in
increase host submission overhead even when kernel counts and GPU work stay the means the callback enqueues replayable device work: it has no required
same. Whole-plan CUDA Graph capture remains pending. per-execution host side effects or decisions based on tensor contents, and any
private streams rejoin the borrowed stream. Lazy setup must complete during the
first ordinary execution. Tensor metadata, attributes, and host plan parameters
remain frozen until scheduler reallocation; tensor contents may change normally.
When every selected extension opts in, the CUDA backend can capture the whole
scheduled plan, including its native fragments, into one CUDA Graph. The first
execution warms up kernels and library state while retaining native fragment
caches. The next stable execution captures
and launches the graph; later calls replay it. Changing a tensor address or the
execution stream invalidates the capture and repeats warmup, even when the graph
UID is unchanged. Capture failures propagate without retrying partially executed
work. An enclosing stream capture records the plan directly instead of starting
a nested capture.
`GGML_EXT_KERNEL_REPLAY_REUSABLE` additionally permits reuse across scheduler
plan rebuilds and requires `CAPTURE_SAFE`. Recorded work must depend only on
complete tensor metadata, addresses, the device and immutable registration data.
Captured resources must belong to the backend or the supplied workspace, never
per-plan host state. Workspace contents must be initialized by recorded device
work on each execution. Prepare/destroy still run for every rebuilt host plan.
The backend cache retains only the executor and workspace; it compares graph
structure, tensor metadata/addresses, selected kernels and workspace requirements
exactly, without dereferencing old graph objects. A new metadata arena can reuse
an equivalent capture. Each backend retains at most 16 retired plans and 256 MiB
of their workspaces, evicting the oldest entries first. Larger workspaces retain
plan-local capture only. Backend destruction synchronizes and releases the cache.
This allows sd.cpp's per-step scheduler rebuilds to retain device replay.
Mixed plans containing an extension without the flag retain ordinary dispatch.
Evaluation callbacks, multiple pipeline copies, and direct backend graph calls
also retain their previous behavior. `GGML_CUDA_DISABLE_GRAPHS` disables whole-plan
capture as well as native CUDA Graph caching. Native operations that cannot be
captured retain the existing fragment execution path. Whole-plan replay requires
CUDA VMM so native scratch addresses remain valid for the backend lifetime;
legacy scratch pools retain the existing fragment path. HIP and MUSA do not yet
provide this plan executor. sd.cpp's CUDA registrations opt in to reusable replay.
The builtin CUDA/Vulkan INT8 GEMM, standalone ConvRot quantization, and remaining fusion rules The builtin CUDA/Vulkan INT8 GEMM, standalone ConvRot quantization, and remaining fusion rules
still need to be externalized. Vulkan execution services and kernels also remain pending. still need to be externalized. Vulkan execution services and kernels also remain pending.
+1 -1
Submodule ggml updated: b34bc42795...924080214e.
+1 -1
View File
@@ -315,6 +315,6 @@ void sd_ggml_register_cuda_convrot(ggml_backend_dev_t device) {
desc.prepare = sd_convrot::prepare; desc.prepare = sd_convrot::prepare;
desc.execute = sd_convrot::execute; desc.execute = sd_convrot::execute;
desc.destroy = sd_convrot::destroy; desc.destroy = sd_convrot::destroy;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }
} }
+3 -3
View File
@@ -773,16 +773,16 @@ void sd_ggml_register_cuda_attention(ggml_backend_dev_t device) {
desc.prepare = attention_prepare; desc.prepare = attention_prepare;
desc.execute = attention_execute; desc.execute = attention_execute;
desc.destroy = attention_destroy; desc.destroy = attention_destroy;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.rms_rope_cute_attention.cuda.v1"; desc.name = "sd.rms_rope_cute_attention.cuda.v1";
desc.op = GGML_OP_RMS_NORM; desc.op = GGML_OP_RMS_NORM;
desc.priority = 300; desc.priority = 300;
desc.supports = nullptr; desc.supports = nullptr;
desc.preferred = nullptr; desc.preferred = nullptr;
desc.optimize = nullptr; desc.optimize = nullptr;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.i8_rope_cute_attention.cuda.v1"; desc.name = "sd.i8_rope_cute_attention.cuda.v1";
desc.op = GGML_OP_MUL_MAT; desc.op = GGML_OP_MUL_MAT;
desc.priority = 400; desc.priority = 400;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }
+2 -2
View File
@@ -456,10 +456,10 @@ void sd_ggml_register_cuda_i8_gated_residual(ggml_backend_dev_t device) {
desc.prepare = sd_i8_gated::prepare; desc.prepare = sd_i8_gated::prepare;
desc.execute = sd_i8_gated::execute; desc.execute = sd_i8_gated::execute;
desc.destroy = sd_i8_gated::destroy; desc.destroy = sd_i8_gated::destroy;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.custom_i8_gated_residual.cuda.v1"; desc.name = "sd.custom_i8_gated_residual.cuda.v1";
desc.op = GGML_OP_CUSTOM; desc.op = GGML_OP_CUSTOM;
desc.custom_op = sd_ggml_i8_op(); desc.custom_op = sd_ggml_i8_op();
desc.optimize = nullptr; desc.optimize = nullptr;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }
+1 -1
View File
@@ -385,5 +385,5 @@ void sd_ggml_register_cuda_i8_projection(ggml_backend_dev_t device) {
desc.prepare = projection_prepare; desc.prepare = projection_prepare;
desc.execute = projection_execute; desc.execute = projection_execute;
desc.destroy = projection_destroy; desc.destroy = projection_destroy;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }
+2 -2
View File
@@ -559,9 +559,9 @@ void sd_ggml_register_cuda_i8(ggml_backend_dev_t device) {
desc.prepare = sd_i8_cuda::prepare; desc.prepare = sd_i8_cuda::prepare;
desc.execute = sd_i8_cuda::execute; desc.execute = sd_i8_cuda::execute;
desc.destroy = sd_i8_cuda::destroy; desc.destroy = sd_i8_cuda::destroy;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.quantize_i8_convrot.cuda.v1"; desc.name = "sd.quantize_i8_convrot.cuda.v1";
desc.custom_op = sd_ggml_convrot_op(); desc.custom_op = sd_ggml_convrot_op();
desc.supports = sd_i8_cuda::convrot_supports; desc.supports = sd_i8_cuda::convrot_supports;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }
+3 -3
View File
@@ -325,16 +325,16 @@ void sd_ggml_register_cuda_rope(ggml_backend_dev_t device) {
desc.prepare = rope_prepare; desc.prepare = rope_prepare;
desc.destroy = rope_destroy; desc.destroy = rope_destroy;
desc.execute = rope_execute; desc.execute = rope_execute;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.rope_apply.cuda.override.v1"; desc.name = "sd.rope_apply.cuda.override.v1";
desc.op = GGML_OP_ROPE_APPLY; desc.op = GGML_OP_ROPE_APPLY;
desc.custom_op = nullptr; desc.custom_op = nullptr;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
desc.name = "sd.rms_norm_mul_rope.cuda.v1"; desc.name = "sd.rms_norm_mul_rope.cuda.v1";
desc.op = GGML_OP_RMS_NORM; desc.op = GGML_OP_RMS_NORM;
desc.priority = 250; desc.priority = 250;
desc.supports = nullptr; desc.supports = nullptr;
desc.match = rope_match; desc.match = rope_match;
desc.optimize = rope_optimize; desc.optimize = rope_optimize;
GGML_ASSERT(ggml_ext_register_kernel(device, &desc)); GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE));
} }