Compare commits

..

6 Commits

35 changed files with 758 additions and 247 deletions

View File

@ -95,6 +95,8 @@ option(SD_MUSA "sd: musa backend" OFF)
option(SD_BUILD_SHARED_LIBS "sd: build shared libs" OFF)
option(SD_BUILD_SHARED_GGML_LIB "sd: build ggml as a separate shared lib" OFF)
option(SD_USE_SYSTEM_GGML "sd: use system-installed GGML library" OFF)
option(SD_USE_UPSTREAM_GGML "sd: build with upstream GGML instead of the patched GGML extensions" OFF)
set(SD_GGML_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ggml" CACHE PATH "sd: ggml source directory (also supplies private headers for system ggml)")
#option(SD_BUILD_SERVER "sd: build server example" ON)
set(CMAKE_C_STANDARD 11)
@ -325,27 +327,19 @@ if (NOT SD_USE_SYSTEM_GGML)
endif()
# deps
# Only add ggml if it hasn't been added yet
if (NOT TARGET ggml)
if (SD_USE_SYSTEM_GGML)
find_package(ggml REQUIRED)
if (NOT ggml_FOUND)
message(FATAL_ERROR "System-installed GGML library not found.")
endif()
add_library(ggml ALIAS ggml::ggml)
else()
add_subdirectory(ggml)
endif()
endif()
include(cmake/ggml.cmake)
add_subdirectory(thirdparty)
target_sources(${SD_LIB} PRIVATE $<TARGET_OBJECTS:zip>)
target_link_libraries(${SD_LIB} PUBLIC ggml)
find_package(Threads REQUIRED)
target_link_libraries(${SD_LIB} PRIVATE Threads::Threads)
target_link_libraries(${SD_LIB} PRIVATE onig sd-utf8proc)
if (SD_CUDA)
find_package(CUDAToolkit REQUIRED)
target_link_libraries(${SD_LIB} PRIVATE CUDA::cuda_driver)
# Keep the driver stub on downstream link lines when no driver is installed.
target_link_libraries(${SD_LIB} PUBLIC CUDA::cuda_driver)
set_property(SOURCE src/core/ggml_extend_backend.cpp APPEND PROPERTY COMPILE_DEFINITIONS SD_USE_CUDA)
endif()
target_include_directories(${SD_LIB} PUBLIC . src include)

27
cmake/ggml.cmake Normal file
View File

@ -0,0 +1,27 @@
if(NOT TARGET ggml AND NOT TARGET ggml::ggml)
if(SD_USE_SYSTEM_GGML)
find_package(ggml REQUIRED)
else()
add_subdirectory("${SD_GGML_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/ggml")
endif()
endif()
if(NOT TARGET ggml)
add_library(ggml ALIAS ggml::ggml)
endif()
get_target_property(sd_ggml_imported ggml IMPORTED)
if(sd_ggml_imported)
set(sd_ggml_private_include "${SD_GGML_SOURCE_DIR}/src")
else()
get_target_property(sd_ggml_private_include ggml SOURCE_DIR)
endif()
if(NOT EXISTS "${sd_ggml_private_include}/ggml-impl.h")
message(FATAL_ERROR "Set SD_GGML_SOURCE_DIR to the source tree matching the selected ggml library (ggml-impl.h is required).")
endif()
target_include_directories(${SD_LIB} PRIVATE "${sd_ggml_private_include}")
set_property(TARGET ${SD_LIB} PROPERTY SD_GGML_PRIVATE_INCLUDE_DIR "${sd_ggml_private_include}")
if(SD_USE_UPSTREAM_GGML)
target_compile_definitions(${SD_LIB} PUBLIC SD_USE_UPSTREAM_GGML)
message(WARNING "Using upstream GGML: FP8 and INT8 tensorwise/convrot are disabled. Some operators may be unsupported and performance may be lower than with patched GGML.")
endif()

View File

@ -10,7 +10,8 @@ set(SD_BIN_DIR "@PACKAGE_SD_BIN_INSTALL_DIR@")
include(CMakeFindDependencyMacro)
find_dependency(ggml REQUIRED HINTS "${SD_LIB_DIR}/cmake")
if(@SD_CUDA@ AND NOT SD_SHARED_LIB)
find_dependency(Threads REQUIRED)
if(@SD_CUDA@)
find_dependency(CUDAToolkit REQUIRED)
endif()
@ -25,13 +26,13 @@ if(NOT TARGET stable-diffusion)
set_target_properties(stable-diffusion
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${SD_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "ggml::ggml"
INTERFACE_LINK_LIBRARIES "ggml::ggml;Threads::Threads"
IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
IMPORTED_LOCATION "${stable-diffusion_LIBRARY}"
INTERFACE_COMPILE_FEATURES "c_std_11;cxx_std_17"
POSITION_INDEPENDENT_CODE ON)
if(@SD_CUDA@ AND NOT SD_SHARED_LIB)
if(@SD_CUDA@)
set_property(TARGET stable-diffusion APPEND PROPERTY INTERFACE_LINK_LIBRARIES CUDA::cuda_driver)
endif()

View File

@ -7,5 +7,5 @@ Name: stable-diffusion
Description: Diffusion model(SD,Flux,Wan,Qwen Image,Z-Image,...) inference in pure C/C++
Version: @SDCPP_BUILD_VERSION@
Libs: -L${libdir} -lstable-diffusion
Libs.private: -lggml -lggml-base
Libs.private: -lggml -lggml-base @CMAKE_THREAD_LIBS_INIT@
Cflags: -I${includedir}

View File

@ -16,6 +16,38 @@ git submodule init
git submodule update
```
## Selecting a GGML source tree
By default, sd.cpp builds the patched GGML submodule in `ggml/`. To build with
an upstream GGML checkout instead, enable `SD_USE_UPSTREAM_GGML` and set
`SD_GGML_SOURCE_DIR`:
```shell
cmake -S . -B build-upstream -DSD_USE_UPSTREAM_GGML=ON -DSD_GGML_SOURCE_DIR=../ggml-upstream
cmake --build build-upstream --config Release
```
The selected source tree supplies both the library and its private headers.
Backend options such as `-DSD_CUDA=ON` apply to the selected tree as usual.
`SD_USE_UPSTREAM_GGML` defaults to `OFF`, which enables the patched GGML
extensions. Set it to `ON` when using upstream GGML; it selects the compatibility
mode and does not download or replace the GGML source tree. Upstream mode
disables FP8 and INT8 tensorwise/convrot and rejects their model files with an
explicit error. FP8 weight type requests, tensor type rules and conversion
outputs are also rejected; no automatic conversion is performed.
Upstream GGML may lack some operators and performance optimizations provided by
the patched version. A warning is emitted during CMake configuration and when
creating an inference context. Ordinary floating-point and shared GGML
quantization types remain available, subject to backend operator support.
`SD_USE_SYSTEM_GGML=ON` instead links an installed GGML CMake package, located
with `ggml_DIR` or `CMAKE_PREFIX_PATH`. In that mode, `SD_GGML_SOURCE_DIR` must
point to the matching source tree for private headers. The installed library
must use the same ABI settings as sd.cpp, including `GGML_MAX_NAME`.
Set `SD_USE_UPSTREAM_GGML=ON` as well if the installed package is upstream GGML.
## WebP and WebM Support in Examples
The example applications (`examples/cli` and `examples/server`) use `libwebp` to support WebP image I/O, and `examples/cli` can also use `libwebm` for `.webm` video output. Both are enabled by default. WebM output currently reuses `libwebp` to encode each frame as VP8 before muxing with `libwebm`.

View File

@ -2,6 +2,9 @@
sd.cpp can load and execute ComfyUI `int8_tensorwise` safetensors with `convrot` metadata directly. The stored INT8 weights are not converted to another weight type at load time.
This requires the INT8 tensorwise/convrot extensions in the patched GGML.
Builds with `SD_USE_UPSTREAM_GGML=ON` reject these files during loading.
## Checkpoint format
Each quantized linear module contains the following tensors:

View File

@ -624,7 +624,7 @@ ArgOptions SDContextParams::get_options() {
true, &diffusion_conv_direct},
{"",
"--vae-conv-direct",
"use ggml_conv2d_direct in the vae model",
"use direct 2D and 3D convolutions in the vae model",
true, &vae_conv_direct},
};

2
ggml

@ -1 +1 @@
Subproject commit 1e22ec0d04b43afa69963e4b4ea6683535d54d79
Subproject commit c6632cd905401abc58b6f5cdd52d228aa7ca1b88

View File

@ -362,6 +362,9 @@ bool convert_with_components(const char* model_path,
const char* tensor_type_rules,
bool convert_name,
int n_threads) {
if (!validate_tensor_types(output_type, tensor_type_rules)) {
return false;
}
ModelLoader model_loader;
bool loaded_any = false;

View File

@ -11,7 +11,7 @@
#include "core/ggml_graph_cut.h"
#include "core/util.h"
#include "ggml-cpu.h"
#include "ggml/src/ggml-impl.h"
#include "ggml-impl.h"
namespace sd {
ComputeWorkspace::~ComputeWorkspace() {

View File

@ -1,6 +1,7 @@
#include "core/ggml_extend.h"
#include <cmath>
#include <stdexcept>
#include <utility>
#include "core/ggml_extend_backend.h"
@ -247,6 +248,7 @@ ggml_tensor* ggml_ext_linear_i8_tensorwise(ggml_context* ctx,
ggml_tensor* b,
int convrot_group_size,
float scale) {
#ifndef SD_USE_UPSTREAM_GGML
GGML_ASSERT(x->type == GGML_TYPE_F32 || (x->type == GGML_TYPE_I8 && scale == 1.f));
if (scale != 1.f) {
x = ggml_ext_scale(ctx, x, scale);
@ -270,6 +272,16 @@ ggml_tensor* ggml_ext_linear_i8_tensorwise(ggml_context* ctx,
}
}
return x;
#else
GGML_UNUSED(ctx);
GGML_UNUSED(x);
GGML_UNUSED(w);
GGML_UNUSED(weight_scale);
GGML_UNUSED(b);
GGML_UNUSED(convrot_group_size);
GGML_UNUSED(scale);
throw std::runtime_error("INT8 tensorwise/convrot is not supported by this ggml build");
#endif
}
ggml_tensor* ggml_ext_pad_ext(ggml_context* ctx,
@ -452,8 +464,13 @@ ggml_tensor* ggml_ext_conv_3d(ggml_context* ctx,
int d0,
int d1,
int d2,
bool force_prec_f32) {
if (force_prec_f32) {
bool force_prec_f32,
bool direct) {
if (direct) {
int64_t OC = w->ne[3] / IC;
int64_t N = x->ne[3] / IC;
x = ggml_conv_3d_direct(ctx, w, x, s0, s1, s2, p0, p1, p2, d0, d1, d2, (int)IC, (int)N, (int)OC);
} else if (force_prec_f32) {
ggml_tensor* im2col = ggml_im2col_3d(ctx, w, x, IC, s0, s1, s2, p0, p1, p2, d0, d1, d2, w->type);
int64_t OC = w->ne[3] / IC;

View File

@ -153,7 +153,8 @@ ggml_tensor* ggml_ext_conv_3d(ggml_context* ctx,
int d0 = 1,
int d1 = 1,
int d2 = 1,
bool force_prec_f32 = false);
bool force_prec_f32 = false,
bool direct = false);
// w: [OC,IC, KD, 1 * 1]
// x: [N, IC, ID, IH*IW]

View File

@ -13,7 +13,7 @@
#endif
#include "core/util.h"
#include "ggml/src/ggml-impl.h"
#include "ggml-impl.h"
#include "stable-diffusion.h"
static std::string trim_copy(const std::string& value) {

View File

@ -16,7 +16,7 @@
#include "ggml-alloc.h"
#include "ggml-backend.h"
#include "ggml/src/ggml-impl.h"
#include "ggml-impl.h"
namespace sd::ggml_graph_cut {
@ -426,8 +426,8 @@ namespace sd::ggml_graph_cut {
if (tensor == nullptr || tensor->name[0] == '\0') {
return false;
}
return starts_with(tensor->name, GGML_RUNNER_CUT_PREFIX) &&
ends_with(tensor->name, GGML_RUNNER_CUT_SUFFIX);
return std::strncmp(tensor->name, GGML_RUNNER_CUT_PREFIX, std::strlen(GGML_RUNNER_CUT_PREFIX)) == 0 &&
tensor->name[std::strlen(tensor->name) - 1] == GGML_RUNNER_CUT_SUFFIX[0];
}
std::string make_graph_cut_name(const std::string& group, const std::string& output) {
@ -492,35 +492,88 @@ namespace sd::ggml_graph_cut {
}
}
std::vector<uint64_t> graph_layout(ggml_cgraph* graph, bool include_bindings) {
std::vector<const ggml_tensor*> tensors;
std::unordered_map<const ggml_tensor*, size_t> indices;
auto add = [&](const ggml_tensor* tensor) {
if (tensor != nullptr && indices.emplace(tensor, tensors.size() + 1).second) {
tensors.push_back(tensor);
}
struct GraphLayoutTensors {
struct Entry {
const ggml_tensor* tensor = nullptr;
size_t index = 0;
};
std::vector<ggml_tensor*> tensors;
std::vector<Entry> entries;
explicit GraphLayoutTensors(size_t graph_size) {
tensors.reserve(graph_size);
size_t capacity = 2;
while (capacity < 2 * graph_size) {
capacity *= 2;
}
entries.resize(capacity);
}
size_t find(const ggml_tensor* tensor) const {
size_t hash = reinterpret_cast<uintptr_t>(tensor) >> 4;
hash ^= hash >> 16;
const size_t mask = entries.size() - 1;
size_t slot = hash & mask;
while (entries[slot].tensor != nullptr && entries[slot].tensor != tensor) {
slot = (slot + 1) & mask;
}
return slot;
}
void add(ggml_tensor* tensor) {
if (tensor == nullptr) {
return;
}
size_t slot = find(tensor);
if (entries[slot].tensor != nullptr) {
return;
}
if (2 * (tensors.size() + 1) > entries.size()) {
// Segment graphs can reference tensors outside their node and leaf arrays.
std::vector<Entry> next(2 * entries.size());
entries.swap(next);
for (size_t i = 0; i < tensors.size(); ++i) {
entries[find(tensors[i])] = {tensors[i], i + 1};
}
slot = find(tensor);
}
entries[slot] = {tensor, tensors.size() + 1};
tensors.push_back(tensor);
}
size_t index(const ggml_tensor* tensor) const {
return tensor == nullptr ? 0 : entries[find(tensor)].index;
}
};
std::vector<uint64_t> graph_layout(ggml_cgraph* graph, bool include_bindings) {
const size_t graph_size = static_cast<size_t>(graph->n_leafs) + graph->n_nodes;
GraphLayoutTensors layout_tensors(graph_size);
const auto& tensors = layout_tensors.tensors;
for (int i = 0; i < graph->n_leafs; ++i) {
add(graph->leafs[i]);
layout_tensors.add(graph->leafs[i]);
}
for (int i = 0; i < graph->n_nodes; ++i) {
add(graph->nodes[i]);
layout_tensors.add(graph->nodes[i]);
}
for (size_t i = 0; i < tensors.size(); ++i) {
add(tensors[i]->view_src);
layout_tensors.add(tensors[i]->view_src);
for (auto source : tensors[i]->src) {
add(source);
layout_tensors.add(source);
}
}
std::vector<uint64_t> signature;
signature.reserve(tensors.size() * 24);
const size_t tensor_fields = 5 + 2 * GGML_MAX_DIMS + GGML_MAX_SRC +
GGML_MAX_OP_PARAMS / sizeof(int32_t) + (include_bindings ? 2 : 0);
signature.reserve(2 + graph_size + tensors.size() * tensor_fields);
signature.push_back(graph->n_nodes);
signature.push_back(graph->n_leafs);
for (int i = 0; i < graph->n_leafs; ++i) {
signature.push_back(indices.at(graph->leafs[i]));
signature.push_back(layout_tensors.index(graph->leafs[i]));
}
for (int i = 0; i < graph->n_nodes; ++i) {
signature.push_back(indices.at(graph->nodes[i]));
signature.push_back(layout_tensors.index(graph->nodes[i]));
}
for (auto tensor : tensors) {
signature.push_back(tensor->op);
@ -536,9 +589,9 @@ namespace sd::ggml_graph_cut {
signature.push_back(tensor->ne[d]);
signature.push_back(tensor->nb[d]);
}
signature.push_back(tensor->view_src == nullptr ? 0 : indices.at(tensor->view_src));
signature.push_back(layout_tensors.index(tensor->view_src));
for (auto source : tensor->src) {
signature.push_back(source == nullptr ? 0 : indices.at(source));
signature.push_back(layout_tensors.index(source));
}
if (!can_ignore_op_params(tensor->op)) {
for (int value : tensor->op_params) {
@ -562,14 +615,18 @@ namespace sd::ggml_graph_cut {
return false;
}
}
std::vector<std::pair<int, std::string>> cut_markers;
for (int i = 0; i < ggml_graph_n_nodes(gf); ++i) {
auto node = ggml_graph_node(gf, i);
size_t cut_index = 0;
for (int i = 0; i < gf->n_nodes; ++i) {
auto node = gf->nodes[i];
if (is_graph_cut_tensor(node)) {
cut_markers.emplace_back(i, node->name);
if (cut_index >= plan.cut_markers.size() ||
plan.cut_markers[cut_index].first != i || plan.cut_markers[cut_index].second != node->name) {
return false;
}
++cut_index;
}
}
return cut_markers == plan.cut_markers;
return cut_index == plan.cut_markers.size();
}
bool plan_matches_graph(ggml_cgraph* gf, const Plan& plan) {
@ -948,11 +1005,11 @@ namespace sd::ggml_graph_cut {
return plan;
}
Plan resolve_plan(ggml_backend_t backend,
ggml_cgraph* gf,
PlanCache* cache,
const std::unordered_set<const ggml_tensor*>& params_tensor_set,
const char* log_desc) {
const Plan& resolve_plan(ggml_backend_t backend,
ggml_cgraph* gf,
PlanCache* cache,
const std::unordered_set<const ggml_tensor*>& params_tensor_set,
const char* log_desc) {
GGML_ASSERT(backend != nullptr);
GGML_ASSERT(gf != nullptr);
GGML_ASSERT(cache != nullptr);

View File

@ -94,11 +94,12 @@ namespace sd::ggml_graph_cut {
ggml_cgraph* gf,
const std::unordered_set<const ggml_tensor*>& params_tensor_set,
const char* log_desc);
Plan resolve_plan(ggml_backend_t backend,
ggml_cgraph* gf,
PlanCache* cache,
const std::unordered_set<const ggml_tensor*>& params_tensor_set,
const char* log_desc);
// The returned reference is valid until its cache entry is evicted or the cache is destroyed.
const Plan& resolve_plan(ggml_backend_t backend,
ggml_cgraph* gf,
PlanCache* cache,
const std::unordered_set<const ggml_tensor*>& params_tensor_set,
const char* log_desc);
} // namespace sd::ggml_graph_cut

View File

@ -342,21 +342,17 @@ void GGMLRunner::copy_data_to_backend_tensor(ggml_cgraph* gf, bool clear_after_c
}
}
bool GGMLRunner::resolve_graph_cut_plan(ggml_cgraph* gf,
GraphCutPlan* plan_out) {
GGML_ASSERT(plan_out != nullptr);
const GGMLRunner::GraphCutPlan& GGMLRunner::resolve_graph_cut_plan(ggml_cgraph* gf) {
GGML_ASSERT(gf != nullptr);
*plan_out = sd::ggml_graph_cut::resolve_plan(runtime_backend,
gf,
&graph_cut_plan_cache_,
params_tensor_set_,
get_desc().c_str());
return true;
return sd::ggml_graph_cut::resolve_plan(runtime_backend,
gf,
&graph_cut_plan_cache_,
params_tensor_set_,
get_desc().c_str());
}
bool GGMLRunner::resolve_graph_cut_layer_split_plan(ggml_cgraph* gf,
GraphCutPlan* plan_out) {
return resolve_graph_cut_plan(gf, plan_out);
const GGMLRunner::GraphCutPlan& GGMLRunner::resolve_graph_cut_layer_split_plan(ggml_cgraph* gf) {
return resolve_graph_cut_plan(gf);
}
bool GGMLRunner::assign_graph_cut_layer_split_backends(ggml_cgraph* gf) {
@ -369,10 +365,7 @@ bool GGMLRunner::assign_graph_cut_layer_split_backends(ggml_cgraph* gf) {
return false;
}
GraphCutPlan plan;
if (!resolve_graph_cut_layer_split_plan(gf, &plan)) {
return false;
}
const auto& plan = resolve_graph_cut_layer_split_plan(gf);
if (!plan.valid || !plan.has_cuts || plan.segments.size() <= 1) {
auto manager = residency_manager.lock();
if (manager == nullptr) {
@ -530,6 +523,7 @@ GGMLRunnerContext GGMLRunner::get_context() {
runner_ctx.linear_scale = linear_scale;
runner_ctx.attn_scale = attn_scale;
runner_ctx.conv2d_direct_enabled = conv2d_direct_enabled;
runner_ctx.conv3d_direct_enabled = conv3d_direct_enabled;
runner_ctx.circular_x_enabled = circular_x_enabled;
runner_ctx.circular_y_enabled = circular_y_enabled;
runner_ctx.weight_adapter = weight_adapter;
@ -819,24 +813,25 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
if (!assign_graph_cut_layer_split_backends(graph)) {
return std::nullopt;
}
const auto params = collect_used_param_tensors(graph);
ggml_graph_cut::Plan plan;
if (!resolve_graph_cut_plan(graph, &plan)) {
return std::nullopt;
}
const auto full_measurement = measure(graph, plan.compute_buffer_size);
const auto params = collect_used_param_tensors(graph);
const auto& cached_plan = resolve_graph_cut_plan(graph);
const auto full_measurement = measure(graph, cached_plan.compute_buffer_size);
if (full_measurement.buffers.empty()) {
return std::nullopt;
}
auto manager = residency_manager.lock();
const bool segmented = !is_multi_device() && !sd_backend_is_cpu(runtime_backend) &&
manager != nullptr && manager->segmented_compute_enabled() &&
plan.valid && plan.has_cuts && plan.segments.size() > 1 &&
cached_plan.valid && cached_plan.has_cuts && cached_plan.segments.size() > 1 &&
!fits(memory_requests(full_measurement.buffers, cache_.pending_bytes(graph)), params);
ggml_graph_cut::Plan monolithic_plan;
if (!segmented) {
ggml_graph_cut::Segment segment;
monolithic_plan.segments.emplace_back();
auto& segment = monolithic_plan.segments.back();
segment.group_name = "graph";
segment.compute_buffer_size = plan.compute_buffer_size;
segment.compute_buffer_size = cached_plan.compute_buffer_size;
segment.internal_node_indices.reserve(ggml_graph_n_nodes(graph));
segment.input_refs.reserve(ggml_graph_cut::leaf_count(graph));
for (int i = 0; i < ggml_graph_n_nodes(graph); ++i) {
segment.internal_node_indices.push_back(i);
}
@ -849,8 +844,8 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
: ggml_graph_cut::Segment::INPUT_EXTERNAL;
segment.input_refs.push_back(input);
}
plan.segments = {std::move(segment)};
}
const auto& plan = segmented ? cached_plan : monolithic_plan;
const bool segments_changed = plan.segments.size() != logged_segment_count_;
if (segments_changed && (segmented || logged_segment_count_ > 1)) {
LOG_VERBOSE("%s using %zu segment%s", get_desc().c_str(),
@ -909,7 +904,10 @@ std::optional<Tensor<float>> GGMLRunner::execute_graph(ggml_cgraph* graph, int n
auto ensure_capacity = [&]() {
sync_runtime_residency();
auto requests = memory_requests(measurement.buffers, new_cache_bytes);
if (!fits(requests, weights.params(index)) && workspace_.release_excess(measurement)) {
if (fits(requests, weights.params(index))) {
return true;
}
if (workspace_.release_excess(measurement)) {
sync_runtime_residency();
requests = memory_requests(measurement.buffers, new_cache_bytes);
}

View File

@ -71,6 +71,7 @@ struct GGMLRunnerContext {
float linear_scale = 0.f;
float attn_scale = 0.f;
bool conv2d_direct_enabled = false;
bool conv3d_direct_enabled = false;
bool circular_x_enabled = false;
bool circular_y_enabled = false;
ggml_tensor* ip_context = nullptr;
@ -178,6 +179,7 @@ protected:
float linear_scale = 0.f;
float attn_scale = 0.f;
bool conv2d_direct_enabled = false;
bool conv3d_direct_enabled = false;
bool circular_x_enabled = false;
bool circular_y_enabled = false;
@ -263,11 +265,9 @@ protected:
void copy_data_to_backend_tensor(ggml_cgraph* gf, bool clear_after_copy = true);
bool resolve_graph_cut_plan(ggml_cgraph* gf,
GraphCutPlan* plan_out);
const GraphCutPlan& resolve_graph_cut_plan(ggml_cgraph* gf);
bool resolve_graph_cut_layer_split_plan(ggml_cgraph* gf,
GraphCutPlan* plan_out);
const GraphCutPlan& resolve_graph_cut_layer_split_plan(ggml_cgraph* gf);
bool assign_graph_cut_layer_split_backends(ggml_cgraph* gf);
@ -346,6 +346,10 @@ public:
conv2d_direct_enabled = enabled;
}
void set_conv3d_direct_enabled(bool enabled) {
conv3d_direct_enabled = enabled;
}
void set_circular_axes(bool circular_x, bool circular_y) {
circular_x_enabled = circular_x;
circular_y_enabled = circular_y;

143
src/core/parallel.cpp Normal file
View File

@ -0,0 +1,143 @@
#include "core/parallel.h"
#include <algorithm>
#include <condition_variable>
#include <exception>
#include <mutex>
#include <thread>
#include <vector>
namespace sd {
namespace parallel_detail {
thread_local ParallelExecutor* executor = nullptr;
thread_local bool active = false;
}
struct ParallelExecutor::Impl {
struct Worker {
std::condition_variable wake;
std::thread thread;
bool ready = false;
};
ParallelExecutor* owner;
std::mutex invocation_mutex;
std::mutex mutex;
std::condition_variable finished;
std::vector<std::unique_ptr<Worker>> workers;
bool stopping = false;
int pending = 0;
int participants = 1;
int64_t begin = 0;
int64_t count = 0;
const std::function<void(int64_t, int64_t)>* task = nullptr;
std::exception_ptr error;
explicit Impl(ParallelExecutor* owner)
: owner(owner) {}
~Impl() {
{
std::lock_guard<std::mutex> lock(mutex);
stopping = true;
}
for (auto& worker : workers) {
worker->wake.notify_one();
}
for (auto& worker : workers) {
worker->thread.join();
}
}
void execute(int index) {
ParallelScope scope(owner);
parallel_detail::Region region;
const int64_t size = count / participants;
const int64_t extra = count % participants;
const int64_t first = begin + index * size + std::min<int64_t>(index, extra);
const int64_t last = first + size + (index < extra ? 1 : 0);
try {
(*task)(first, last);
} catch (...) {
std::lock_guard<std::mutex> lock(mutex);
if (!error) {
error = std::current_exception();
}
}
}
void worker_loop(Worker* worker, int index) {
std::unique_lock<std::mutex> lock(mutex);
for (;;) {
worker->wake.wait(lock, [&] { return stopping || worker->ready; });
if (stopping) {
return;
}
worker->ready = false;
lock.unlock();
execute(index);
lock.lock();
if (--pending == 0) {
finished.notify_one();
}
}
}
void run(int64_t first, int64_t last, int n_tasks, const std::function<void(int64_t, int64_t)>& callback) {
std::lock_guard<std::mutex> invocation_lock(invocation_mutex);
std::unique_lock<std::mutex> lock(mutex);
while (static_cast<int>(workers.size()) < n_tasks - 1) {
workers.push_back(std::make_unique<Worker>());
auto* worker = workers.back().get();
const int index = static_cast<int>(workers.size());
try {
worker->thread = std::thread([this, worker, index] { worker_loop(worker, index); });
} catch (...) {
workers.pop_back();
throw;
}
}
begin = first;
count = last - first;
participants = n_tasks;
pending = n_tasks - 1;
task = &callback;
error = nullptr;
for (int i = 0; i < pending; ++i) {
workers[i]->ready = true;
workers[i]->wake.notify_one();
}
lock.unlock();
execute(0);
lock.lock();
finished.wait(lock, [&] { return pending == 0; });
task = nullptr;
if (error) {
std::rethrow_exception(error);
}
}
};
ParallelExecutor::ParallelExecutor(int n_threads)
: n_threads_(std::max(1, n_threads)), impl_(std::make_unique<Impl>(this)) {}
ParallelExecutor::~ParallelExecutor() = default;
void ParallelExecutor::run(int64_t begin, int64_t end, int64_t grain_size, const std::function<void(int64_t, int64_t)>& task) {
if (begin < 0 || grain_size <= 0) {
throw std::invalid_argument("parallel_for requires begin >= 0 and grain_size > 0");
}
if (end <= begin) {
return;
}
const int n_tasks = static_cast<int>(std::min<int64_t>(n_threads_, (end - begin) / grain_size));
if (parallel_detail::active || n_tasks <= 1) {
parallel_detail::Region region;
task(begin, end);
return;
}
impl_->run(begin, end, n_tasks, task);
}
}

77
src/core/parallel.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef __SD_CORE_PARALLEL_H__
#define __SD_CORE_PARALLEL_H__
#include <cstdint>
#include <functional>
#include <memory>
#include <stdexcept>
#include <utility>
namespace sd {
class ParallelExecutor {
struct Impl;
int n_threads_;
std::unique_ptr<Impl> impl_;
public:
explicit ParallelExecutor(int n_threads);
~ParallelExecutor();
ParallelExecutor(const ParallelExecutor&) = delete;
ParallelExecutor& operator=(const ParallelExecutor&) = delete;
int num_threads() const { return n_threads_; }
void run(int64_t begin, int64_t end, int64_t grain_size, const std::function<void(int64_t, int64_t)>& task);
};
namespace parallel_detail {
extern thread_local ParallelExecutor* executor;
extern thread_local bool active;
class Region {
bool previous_;
public:
Region()
: previous_(active) { active = true; }
~Region() { active = previous_; }
Region(const Region&) = delete;
Region& operator=(const Region&) = delete;
};
}
class ParallelScope {
ParallelExecutor* previous_;
public:
explicit ParallelScope(ParallelExecutor* executor)
: previous_(parallel_detail::executor) {
parallel_detail::executor = executor;
}
~ParallelScope() { parallel_detail::executor = previous_; }
ParallelScope(const ParallelScope&) = delete;
ParallelScope& operator=(const ParallelScope&) = delete;
};
// Ranges are non-negative. The callback may run concurrently and must own its writes.
template <typename F>
inline void parallel_for(int64_t begin, int64_t end, int64_t grain_size, F&& task) {
if (begin < 0 || grain_size <= 0) {
throw std::invalid_argument("parallel_for requires begin >= 0 and grain_size > 0");
}
if (end <= begin) {
return;
}
auto* executor = parallel_detail::executor;
if (parallel_detail::active || executor == nullptr || executor->num_threads() <= 1 ||
(end - begin) / grain_size < 2) {
parallel_detail::Region region;
task(begin, end);
return;
}
executor->run(begin, end, grain_size, std::forward<F>(task));
}
}
#endif // __SD_CORE_PARALLEL_H__

View File

@ -1,6 +1,7 @@
#ifndef __SD_CORE_RNG_PHILOX_HPP__
#define __SD_CORE_RNG_PHILOX_HPP__
#include <array>
#include <cmath>
#include <vector>
@ -14,60 +15,35 @@ private:
uint32_t offset;
private:
std::vector<uint32_t> philox_m = {0xD2511F53, 0xCD9E8D57};
std::vector<uint32_t> philox_w = {0x9E3779B9, 0xBB67AE85};
float two_pow32_inv = 2.3283064e-10f;
float two_pow32_inv_2pi = 2.3283064e-10f * 6.2831855f;
using Counter = std::array<std::vector<uint32_t>, 4>;
std::vector<std::vector<uint32_t>> uint32(const std::vector<uint64_t>& x) {
uint32_t N = (uint32_t)x.size();
std::vector<std::vector<uint32_t>> result(2, std::vector<uint32_t>(N));
for (uint32_t i = 0; i < N; ++i) {
result[0][i] = static_cast<uint32_t>(x[i] & 0xFFFFFFFF);
result[1][i] = static_cast<uint32_t>(x[i] >> 32);
}
return result;
}
static constexpr uint32_t philox_m[2] = {0xD2511F53, 0xCD9E8D57};
static constexpr uint32_t philox_w[2] = {0x9E3779B9, 0xBB67AE85};
float two_pow32_inv = 2.3283064e-10f;
float two_pow32_inv_2pi = 2.3283064e-10f * 6.2831855f;
// A single round of the Philox 4x32 random number generator.
void philox4_round(std::vector<std::vector<uint32_t>>& counter,
const std::vector<std::vector<uint32_t>>& key) {
void philox4_round(Counter& counter, uint32_t key0, uint32_t key1) {
uint32_t N = (uint32_t)counter[0].size();
for (uint32_t i = 0; i < N; i++) {
const uint64_t v1 = static_cast<uint64_t>(counter[0][i]) * static_cast<uint64_t>(philox_m[0]);
const uint64_t v2 = static_cast<uint64_t>(counter[2][i]) * static_cast<uint64_t>(philox_m[1]);
counter[0][i] = static_cast<uint32_t>(v2 >> 32) ^ counter[1][i] ^ key[0][i];
counter[0][i] = static_cast<uint32_t>(v2 >> 32) ^ counter[1][i] ^ key0;
counter[1][i] = static_cast<uint32_t>(v2);
counter[2][i] = static_cast<uint32_t>(v1 >> 32) ^ counter[3][i] ^ key[1][i];
counter[2][i] = static_cast<uint32_t>(v1 >> 32) ^ counter[3][i] ^ key1;
counter[3][i] = static_cast<uint32_t>(v1);
}
}
// Generates 32-bit random numbers using the Philox 4x32 random number generator.
// Parameters:
// counter : A 4xN array of 32-bit integers representing the counter values (offset into generation).
// key : A 2xN array of 32-bit integers representing the key values (seed).
// rounds : The number of rounds to perform.
// Returns:
// std::vector<std::vector<uint32_t>>: A 4xN array of 32-bit integers containing the generated random numbers.
std::vector<std::vector<uint32_t>> philox4_32(std::vector<std::vector<uint32_t>>& counter,
std::vector<std::vector<uint32_t>>& key,
int rounds = 10) {
uint32_t N = (uint32_t)counter[0].size();
void philox4_32(Counter& counter, uint32_t key0, uint32_t key1, int rounds = 10) {
for (int i = 0; i < rounds - 1; ++i) {
philox4_round(counter, key);
for (uint32_t j = 0; j < N; ++j) {
key[0][j] += philox_w[0];
key[1][j] += philox_w[1];
}
philox4_round(counter, key0, key1);
key0 += philox_w[0];
key1 += philox_w[1];
}
philox4_round(counter, key);
return counter;
philox4_round(counter, key0, key1);
}
float box_muller(float x, float y) {
@ -96,24 +72,22 @@ public:
}
std::vector<float> randn(uint32_t n) override {
std::vector<std::vector<uint32_t>> counter(4, std::vector<uint32_t>(n, 0));
for (uint32_t i = 0; i < n; i++) {
counter[0][i] = this->offset;
}
Counter counter;
counter[0].resize(n, this->offset);
counter[1].resize(n);
counter[2].resize(n);
counter[3].resize(n);
for (uint32_t i = 0; i < n; i++) {
counter[2][i] = i;
}
this->offset += 1;
std::vector<uint64_t> key(n, this->seed);
std::vector<std::vector<uint32_t>> key_uint32 = uint32(key);
philox4_32(counter, static_cast<uint32_t>(this->seed), static_cast<uint32_t>(this->seed >> 32));
std::vector<std::vector<uint32_t>> g = philox4_32(counter, key_uint32);
std::vector<float> result;
std::vector<float> result(n);
for (uint32_t i = 0; i < n; ++i) {
result.push_back(box_muller((float)g[0][i], (float)g[1][i]));
result[i] = box_muller((float)counter[0][i], (float)counter[1][i]);
}
return result;
}

View File

@ -16,6 +16,7 @@
#include <utility>
#include <vector>
#include "core/parallel.h"
#include "core/rng.hpp"
namespace sd {
@ -59,6 +60,15 @@ namespace sd {
return numel;
}
template <typename F>
inline void tensor_for_each(int64_t count, F&& fn, int64_t grain_size = 65536) {
parallel_for(0, count, grain_size, [&](int64_t begin, int64_t end) {
for (int64_t i = begin; i < end; ++i) {
fn(i);
}
});
}
template <typename T>
class Tensor {
public:
@ -230,7 +240,10 @@ namespace sd {
}
void fill_(const T& value) {
std::fill(data_.begin(), data_.end(), value);
const T fill_value = value;
parallel_for(0, numel(), 65536, [&](int64_t begin, int64_t end) {
std::fill_n(data_.data() + begin, end - begin, fill_value);
});
}
Tensor& masked_fill_(const Tensor<uint8_t>& mask, const T& value);
@ -390,7 +403,7 @@ namespace sd {
tensor_shape_to_string(lhs) + ", rhs_shape=" +
tensor_shape_to_string(rhs));
}
shape[i] = std::max(lhs_dim, rhs_dim);
shape[i] = lhs_dim == 1 ? rhs_dim : lhs_dim;
}
return shape;
}
@ -425,39 +438,55 @@ namespace sd {
const std::vector<int64_t>& rhs_shape_raw,
const std::vector<int64_t>& rhs_strides_raw,
F&& fn) {
const size_t ndim = out_shape.size();
std::vector<int64_t> out_strides = tensor_compute_strides(out_shape);
std::vector<int64_t> lhs_shape(ndim, 1);
std::vector<int64_t> lhs_strides(ndim, 0);
std::vector<int64_t> rhs_shape(ndim, 1);
std::vector<int64_t> rhs_strides(ndim, 0);
for (size_t i = 0; i < lhs_shape_raw.size(); ++i) {
lhs_shape[i] = lhs_shape_raw[i];
lhs_strides[i] = lhs_strides_raw[i];
}
for (size_t i = 0; i < rhs_shape_raw.size(); ++i) {
rhs_shape[i] = rhs_shape_raw[i];
rhs_strides[i] = rhs_strides_raw[i];
}
const int64_t numel = tensor_numel(out_shape);
for (int64_t flat = 0; flat < numel; ++flat) {
int64_t remaining = flat;
int64_t lhs_offset = 0;
int64_t rhs_offset = 0;
for (size_t i = ndim; i-- > 0;) {
int64_t coord = remaining / out_strides[i];
remaining %= out_strides[i];
if (lhs_shape[i] != 1) {
lhs_offset += coord * lhs_strides[i];
const int64_t numel = tensor_numel(out_shape);
const size_t ndim = out_shape.size();
auto broadcast_strides = [&](const std::vector<int64_t>& shape,
const std::vector<int64_t>& strides) {
if ((numel != 0 && tensor_numel(shape) == 0) || strides.size() != shape.size()) {
tensor_throw_invalid_argument("Tensor broadcast requires non-empty inputs and matching strides");
}
std::vector<int64_t> result(ndim, 0);
for (size_t i = 0; i < std::max(ndim, shape.size()); ++i) {
const int64_t input_dim = i < shape.size() ? shape[i] : 1;
const int64_t output_dim = i < ndim ? out_shape[i] : 1;
if (input_dim != 1 && input_dim != output_dim) {
tensor_throw_invalid_argument("Tensor broadcast cannot expand the destination: input_shape=" +
tensor_shape_to_string(shape) + ", output_shape=" +
tensor_shape_to_string(out_shape));
}
if (rhs_shape[i] != 1) {
rhs_offset += coord * rhs_strides[i];
if (i < ndim && input_dim != 1) {
result[i] = strides[i];
}
}
fn(flat, lhs_offset, rhs_offset);
return result;
};
const auto lhs_strides = broadcast_strides(lhs_shape_raw, lhs_strides_raw);
const auto rhs_strides = broadcast_strides(rhs_shape_raw, rhs_strides_raw);
if (numel == 0) {
return;
}
parallel_for(0, numel, 16384, [&](int64_t begin, int64_t end) {
auto coord = tensor_unravel_index(begin, out_shape);
int64_t lhs_offset = 0;
int64_t rhs_offset = 0;
for (size_t i = 0; i < ndim; ++i) {
lhs_offset += coord[i] * lhs_strides[i];
rhs_offset += coord[i] * rhs_strides[i];
}
for (int64_t flat = begin; flat < end; ++flat) {
fn(flat, lhs_offset, rhs_offset);
for (size_t i = 0; i < ndim; ++i) {
lhs_offset += lhs_strides[i];
rhs_offset += rhs_strides[i];
if (++coord[i] < out_shape[i]) {
break;
}
coord[i] = 0;
lhs_offset -= out_shape[i] * lhs_strides[i];
rhs_offset -= out_shape[i] * rhs_strides[i];
}
}
});
}
template <typename T>
@ -469,6 +498,7 @@ namespace sd {
const std::vector<int64_t> data_strides = tensor_compute_strides(shape_);
const std::vector<int64_t> mask_strides = tensor_compute_strides(mask.shape());
const uint8_t* mask_data = mask.data();
const T fill_value = value;
tensor_for_each_broadcast_offset(shape_,
shape_,
data_strides,
@ -476,7 +506,7 @@ namespace sd {
mask_strides,
[&](int64_t, int64_t data_offset, int64_t mask_offset) {
if (mask_data[mask_offset] != 0) {
data_[static_cast<size_t>(data_offset)] = value;
data_[static_cast<size_t>(data_offset)] = fill_value;
}
});
return *this;
@ -486,9 +516,9 @@ namespace sd {
inline Tensor<uint8_t> operator<(const Tensor<T>& lhs, Scalar rhs) {
Tensor<uint8_t> result(lhs.shape());
const T value = static_cast<T>(rhs);
for (int64_t i = 0; i < lhs.numel(); ++i) {
result[i] = lhs[i] < value ? 1 : 0;
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
result.data()[i] = lhs.data()[i] < value ? 1 : 0;
});
return result;
}
@ -496,9 +526,9 @@ namespace sd {
inline Tensor<uint8_t> operator<(Scalar lhs, const Tensor<T>& rhs) {
Tensor<uint8_t> result(rhs.shape());
const T value = static_cast<T>(lhs);
for (int64_t i = 0; i < rhs.numel(); ++i) {
result[i] = value < rhs[i] ? 1 : 0;
}
tensor_for_each(rhs.numel(), [&](int64_t i) {
result.data()[i] = value < rhs.data()[i] ? 1 : 0;
});
return result;
}
@ -516,7 +546,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t flat, int64_t lhs_offset, int64_t rhs_offset) {
result[flat] = lhs_data[lhs_offset] < rhs_data[rhs_offset] ? 1 : 0;
result.data()[flat] = lhs_data[lhs_offset] < rhs_data[rhs_offset] ? 1 : 0;
});
return result;
}
@ -524,9 +554,9 @@ namespace sd {
template <typename T>
inline Tensor<T>& operator+=(Tensor<T>& lhs, const Tensor<T>& rhs) {
if (lhs.shape() == rhs.shape()) {
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] += rhs[i];
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] += rhs.data()[i];
});
return lhs;
}
tensor_broadcast_shape(lhs.shape(), rhs.shape());
@ -539,7 +569,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t, int64_t lhs_offset, int64_t rhs_offset) {
lhs[static_cast<int64_t>(lhs_offset)] += rhs_data[rhs_offset];
lhs.data()[lhs_offset] += rhs_data[rhs_offset];
});
return lhs;
}
@ -547,18 +577,18 @@ namespace sd {
template <typename T, typename Scalar, typename = std::enable_if_t<std::is_arithmetic<Scalar>::value>>
inline Tensor<T>& operator+=(Tensor<T>& lhs, Scalar rhs) {
const T value = static_cast<T>(rhs);
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] += value;
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] += value;
});
return lhs;
}
template <typename T>
inline Tensor<T>& operator-=(Tensor<T>& lhs, const Tensor<T>& rhs) {
if (lhs.shape() == rhs.shape()) {
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] -= rhs[i];
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] -= rhs.data()[i];
});
return lhs;
}
tensor_broadcast_shape(lhs.shape(), rhs.shape());
@ -571,7 +601,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t, int64_t lhs_offset, int64_t rhs_offset) {
lhs[static_cast<int64_t>(lhs_offset)] -= rhs_data[rhs_offset];
lhs.data()[lhs_offset] -= rhs_data[rhs_offset];
});
return lhs;
}
@ -579,18 +609,18 @@ namespace sd {
template <typename T, typename Scalar, typename = std::enable_if_t<std::is_arithmetic<Scalar>::value>>
inline Tensor<T>& operator-=(Tensor<T>& lhs, Scalar rhs) {
const T value = static_cast<T>(rhs);
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] -= value;
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] -= value;
});
return lhs;
}
template <typename T>
inline Tensor<T>& operator*=(Tensor<T>& lhs, const Tensor<T>& rhs) {
if (lhs.shape() == rhs.shape()) {
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] *= rhs[i];
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] *= rhs.data()[i];
});
return lhs;
}
tensor_broadcast_shape(lhs.shape(), rhs.shape());
@ -603,7 +633,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t, int64_t lhs_offset, int64_t rhs_offset) {
lhs[static_cast<int64_t>(lhs_offset)] *= rhs_data[rhs_offset];
lhs.data()[lhs_offset] *= rhs_data[rhs_offset];
});
return lhs;
}
@ -611,18 +641,18 @@ namespace sd {
template <typename T, typename Scalar, typename = std::enable_if_t<std::is_arithmetic<Scalar>::value>>
inline Tensor<T>& operator*=(Tensor<T>& lhs, Scalar rhs) {
const T value = static_cast<T>(rhs);
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] *= value;
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] *= value;
});
return lhs;
}
template <typename T>
inline Tensor<T>& operator/=(Tensor<T>& lhs, const Tensor<T>& rhs) {
if (lhs.shape() == rhs.shape()) {
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] /= rhs[i];
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] /= rhs.data()[i];
});
return lhs;
}
tensor_broadcast_shape(lhs.shape(), rhs.shape());
@ -635,7 +665,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t, int64_t lhs_offset, int64_t rhs_offset) {
lhs[static_cast<int64_t>(lhs_offset)] /= rhs_data[rhs_offset];
lhs.data()[lhs_offset] /= rhs_data[rhs_offset];
});
return lhs;
}
@ -643,9 +673,9 @@ namespace sd {
template <typename T, typename Scalar, typename = std::enable_if_t<std::is_arithmetic<Scalar>::value>>
inline Tensor<T>& operator/=(Tensor<T>& lhs, Scalar rhs) {
const T value = static_cast<T>(rhs);
for (int64_t i = 0; i < lhs.numel(); ++i) {
lhs[i] /= value;
}
tensor_for_each(lhs.numel(), [&](int64_t i) {
lhs.data()[i] /= value;
});
return lhs;
}
@ -664,7 +694,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t flat, int64_t lhs_offset, int64_t rhs_offset) {
result[flat] = lhs_data[lhs_offset] + rhs_data[rhs_offset];
result.data()[flat] = lhs_data[lhs_offset] + rhs_data[rhs_offset];
});
return result;
}
@ -699,7 +729,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t flat, int64_t lhs_offset, int64_t rhs_offset) {
result[flat] = lhs_data[lhs_offset] - rhs_data[rhs_offset];
result.data()[flat] = lhs_data[lhs_offset] - rhs_data[rhs_offset];
});
return result;
}
@ -717,9 +747,9 @@ namespace sd {
inline Tensor<T> operator-(Scalar lhs, const Tensor<T>& rhs) {
Tensor<T> result = rhs;
const T value = static_cast<T>(lhs);
for (int64_t i = 0; i < result.numel(); ++i) {
result[i] = value - result[i];
}
tensor_for_each(result.numel(), [&](int64_t i) {
result.data()[i] = value - result.data()[i];
});
return result;
}
@ -738,7 +768,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t flat, int64_t lhs_offset, int64_t rhs_offset) {
result[flat] = lhs_data[lhs_offset] * rhs_data[rhs_offset];
result.data()[flat] = lhs_data[lhs_offset] * rhs_data[rhs_offset];
});
return result;
}
@ -773,7 +803,7 @@ namespace sd {
rhs.shape(),
rhs_strides,
[&](int64_t flat, int64_t lhs_offset, int64_t rhs_offset) {
result[flat] = lhs_data[lhs_offset] / rhs_data[rhs_offset];
result.data()[flat] = lhs_data[lhs_offset] / rhs_data[rhs_offset];
});
return result;
}
@ -791,18 +821,18 @@ namespace sd {
inline Tensor<T> operator/(Scalar lhs, const Tensor<T>& rhs) {
Tensor<T> result = rhs;
const T value = static_cast<T>(lhs);
for (int64_t i = 0; i < result.numel(); ++i) {
result[i] = value / result[i];
}
tensor_for_each(result.numel(), [&](int64_t i) {
result.data()[i] = value / result.data()[i];
});
return result;
}
template <typename T>
inline Tensor<T> operator-(const Tensor<T>& tensor) {
Tensor<T> result = tensor;
for (int64_t i = 0; i < result.numel(); ++i) {
result[i] = -result[i];
}
tensor_for_each(result.numel(), [&](int64_t i) {
result.data()[i] = -result.data()[i];
});
return result;
}
@ -1067,9 +1097,11 @@ namespace sd {
template <typename T>
inline Tensor<T> exp(const Tensor<T>& input) {
Tensor<T> output(input.shape());
for (int64_t i = 0; i < input.numel(); ++i) {
output[i] = static_cast<T>(std::exp(static_cast<double>(input[i])));
}
tensor_for_each(
input.numel(), [&](int64_t i) {
output.data()[i] = static_cast<T>(std::exp(static_cast<double>(input.data()[i])));
},
4096);
return output;
}
@ -1079,18 +1111,18 @@ namespace sd {
tensor_throw_invalid_argument("Tensor clamp requires min_value <= max_value");
}
Tensor<T> output(input.shape());
for (int64_t i = 0; i < input.numel(); ++i) {
output[i] = std::clamp(input[i], min_value, max_value);
}
tensor_for_each(input.numel(), [&](int64_t i) {
output.data()[i] = std::clamp(input.data()[i], min_value, max_value);
});
return output;
}
template <typename T>
inline Tensor<T> round(const Tensor<T>& input) {
Tensor<T> output(input.shape());
for (int64_t i = 0; i < input.numel(); ++i) {
output[i] = static_cast<T>(std::round(static_cast<double>(input[i])));
}
tensor_for_each(input.numel(), [&](int64_t i) {
output.data()[i] = static_cast<T>(std::round(static_cast<double>(input.data()[i])));
});
return output;
}

View File

@ -414,14 +414,43 @@ std::vector<std::string> split_string(const std::string& str, char delimiter) {
}
ggml_type sd_type_to_ggml_type(sd_type_t sdtype) {
if (sdtype == SD_TYPE_F8_E4M3 || sdtype == SD_TYPE_F8_E5M2) {
#ifndef SD_USE_UPSTREAM_GGML
return sdtype == SD_TYPE_F8_E4M3 ? GGML_TYPE_F8_E4M3 : GGML_TYPE_F8_E5M2;
#else
return GGML_TYPE_COUNT;
#endif
}
const int type_value = static_cast<int>(sdtype);
if (type_value < std::min<int>(SD_TYPE_COUNT, GGML_TYPE_COUNT)) {
if (type_value >= 0 && type_value < std::min<int>(SD_TYPE_COUNT, GGML_TYPE_COUNT)) {
return static_cast<ggml_type>(type_value);
} else {
return GGML_TYPE_COUNT;
}
}
bool validate_tensor_types(sd_type_t type, const char* tensor_type_rules) {
if (type != SD_TYPE_COUNT && sd_type_to_ggml_type(type) == GGML_TYPE_COUNT) {
LOG_ERROR("weight type %s is not supported by this ggml build", sd_type_name(type));
return false;
}
#ifdef SD_USE_UPSTREAM_GGML
for (const auto& rule : split_string(SAFE_STR(tensor_type_rules), ',')) {
const auto pos = rule.find('=');
if (pos != std::string::npos) {
const auto name = rule.substr(pos + 1);
if (name == "f8_e4m3" || name == "f8_e5m2") {
LOG_ERROR("FP8 is not supported by this ggml build (tensor type rule '%s')", rule.c_str());
return false;
}
}
}
#else
GGML_UNUSED(tensor_type_rules);
#endif
return true;
}
KeyValueArgs parse_key_value_args(const char* args, const char* context) {
KeyValueArgs pairs;
@ -755,6 +784,13 @@ sd::Tensor<float> clip_preprocess(const sd::Tensor<float>& image, int target_wid
int64_t resized_width = static_cast<int64_t>(scale * static_cast<float>(image.shape()[0]));
int64_t resized_height = static_cast<int64_t>(scale * static_cast<float>(image.shape()[1]));
// The resized image must cover the crop window. Floating-point rounding can
// leave a side one pixel short of the crop target (e.g. 730 -> 735.999...
// -> 735 after truncation), so clamp to keep the center crop in bounds.
// Truncation is otherwise preserved to avoid changing existing results.
resized_width = std::max<int64_t>(resized_width, target_width);
resized_height = std::max<int64_t>(resized_height, target_height);
sd::Tensor<float> resized = sd::ops::interpolate(
image,
{resized_width, resized_height, image.shape()[2], image.shape()[3]});

View File

@ -90,6 +90,7 @@ void log_printf(sd_log_level_t level, const char* file, int line, const char* fo
void sd_ggml_log_callback(ggml_log_level level, const char* text, void*);
ggml_type sd_type_to_ggml_type(sd_type_t sdtype);
bool validate_tensor_types(sd_type_t type, const char* tensor_type_rules);
std::string trim(const std::string& s);

View File

@ -208,6 +208,7 @@ public:
ggml_tensor* w = params["weight"];
const float scale = ctx->linear_scale > 0.f ? ctx->linear_scale : this->scale;
ggml_tensor* weight_scale = has_weight_scale ? params["weight_scale"] : nullptr;
#ifndef SD_USE_UPSTREAM_GGML
if (w->type == GGML_TYPE_F8_E4M3 || w->type == GGML_TYPE_F8_E5M2) {
bool supports_fp8_matmul = false;
if (ctx->backend != nullptr) {
@ -221,6 +222,7 @@ public:
w = ggml_cast(ctx->ggml_ctx, w, GGML_TYPE_BF16);
}
}
#endif
ggml_tensor* b = nullptr;
if (bias) {
b = params["bias"];
@ -238,6 +240,7 @@ public:
if (ctx->weight_adapter && b != nullptr) {
b = ctx->weight_adapter->patch_weight(ctx->ggml_ctx, ctx->backend, b, prefix + "bias");
}
#ifndef SD_USE_UPSTREAM_GGML
if (int8_convrot && scale == 1.f) {
const auto cache_key = std::make_pair(x, int8_convrot_group_size);
auto cached = ctx->int8_convrot_cache.find(cache_key);
@ -248,6 +251,7 @@ public:
x = cached->second;
}
}
#endif
out = ggml_ext_linear_i8_tensorwise(ctx->ggml_ctx,
x,
w,
@ -728,7 +732,7 @@ public:
std::get<2>(stride), std::get<1>(stride), std::get<0>(stride),
std::get<2>(padding), std::get<1>(padding), std::get<0>(padding),
std::get<2>(dilation), std::get<1>(dilation), std::get<0>(dilation),
force_prec_f32);
force_prec_f32, ctx->conv3d_direct_enabled);
}
};

View File

@ -78,7 +78,8 @@ namespace WAN {
return ggml_ext_conv_3d(ctx->ggml_ctx, ctx->backend, x, w, b, in_channels,
std::get<2>(stride), std::get<1>(stride), std::get<0>(stride),
0, 0, 0,
std::get<2>(dilation), std::get<1>(dilation), std::get<0>(dilation));
std::get<2>(dilation), std::get<1>(dilation), std::get<0>(dilation),
false, ctx->conv3d_direct_enabled);
}
};

View File

@ -57,6 +57,18 @@ bool read_gguf_file(const std::string& file_path,
size_t data_offset = gguf_reader.data_offset();
for (const auto& gguf_tensor_info : gguf_reader.tensors()) {
#ifdef SD_USE_UPSTREAM_GGML
if (static_cast<int>(gguf_tensor_info.type) == SD_TYPE_F8_E4M3 ||
static_cast<int>(gguf_tensor_info.type) == SD_TYPE_F8_E5M2) {
set_error(error, "FP8 is not supported by this ggml build (tensor '" + gguf_tensor_info.name + "')");
return false;
}
#endif
if (static_cast<unsigned>(gguf_tensor_info.type) >= GGML_TYPE_COUNT ||
ggml_get_type_traits(gguf_tensor_info.type)->type_size == 0) {
set_error(error, "unsupported GGUF tensor type (tensor '" + gguf_tensor_info.name + "')");
return false;
}
TensorStorage tensor_storage(
gguf_tensor_info.name,
gguf_tensor_info.type,

View File

@ -86,10 +86,12 @@ static ggml_type safetensors_dtype_to_ggml_type(const std::string& dtype) {
ttype = GGML_TYPE_F32;
} else if (dtype == "F64") {
ttype = GGML_TYPE_F32;
#ifndef SD_USE_UPSTREAM_GGML
} else if (dtype == "F8_E4M3") {
ttype = GGML_TYPE_F8_E4M3;
} else if (dtype == "F8_E5M2") {
ttype = GGML_TYPE_F8_E5M2;
#endif
} else if (dtype == "I32") {
ttype = GGML_TYPE_I32;
} else if (dtype == "I64") {
@ -230,6 +232,12 @@ bool read_safetensors_file(const std::string& file_path,
if (!read_comfy_quant_config(file, file_path, name, data_start + begin, end - begin, config, error)) {
return false;
}
#ifdef SD_USE_UPSTREAM_GGML
if (config.format == "int8_tensorwise") {
set_error(error, "INT8 tensorwise/convrot is not supported by this ggml build (tensor '" + name + "')");
return false;
}
#endif
const std::string module_name = name.substr(0, name.size() - std::string(".comfy_quant").size());
comfy_quant_configs.emplace(module_name, std::move(config));
}
@ -279,6 +287,12 @@ bool read_safetensors_file(const std::string& file_path,
continue;
}
#ifdef SD_USE_UPSTREAM_GGML
if (dtype == "F8_E4M3" || dtype == "F8_E5M2") {
set_error(error, "FP8 is not supported by this ggml build (tensor '" + name + "')");
return false;
}
#endif
ggml_type type = safetensors_dtype_to_ggml_type(dtype);
if (type == GGML_TYPE_COUNT) {
set_error(error, "unsupported dtype '" + dtype + "' (tensor '" + name + "')");

View File

@ -274,6 +274,7 @@ bool ModelManager::register_param_tensors(ModelComponent component,
new_states.push_back(std::move(state));
}
resolved_tensor_states_.clear();
for (auto& state : new_states) {
TensorState* registered_state = state.get();
tensor_states_by_tensor_[registered_state->tensor] = registered_state;
@ -369,6 +370,7 @@ bool ModelManager::unregister_tensor_states(const std::unordered_set<TensorState
}
}
resolved_tensor_states_.clear();
for (auto it = tensor_states_by_tensor_.begin(); it != tensor_states_by_tensor_.end();) {
if (target_states.count(it->second) > 0) {
it = tensor_states_by_tensor_.erase(it);
@ -1199,22 +1201,52 @@ bool ModelManager::resolve_required_tensor_states(const std::vector<ggml_tensor*
std::vector<TensorState*>& required_states,
ggml_backend_t compute_backend) const {
required_states.clear();
required_states.reserve(tensors.size());
auto append_states = [&](const std::vector<TensorState*>& states) {
for (TensorState* state : states) {
if (compute_backend == nullptr || state->compute_backend == nullptr ||
state->compute_backend == compute_backend) {
required_states.push_back(state);
}
}
};
for (auto it = resolved_tensor_states_.begin(); it != resolved_tensor_states_.end(); ++it) {
if (it->tensors == tensors) {
append_states(it->states);
resolved_tensor_states_.splice(resolved_tensor_states_.begin(), resolved_tensor_states_, it);
return true;
}
}
std::vector<TensorState*> states;
states.reserve(tensors.size());
std::unordered_set<TensorState*> seen;
seen.reserve(tensors.size());
bool cacheable = true;
for (ggml_tensor* tensor : tensors) {
if (tensor == nullptr) {
continue;
}
auto param = resolve_param_tensor(tensor);
auto found = tensor_states_by_tensor_.find(param);
auto found = tensor_states_by_tensor_.find(tensor);
// Unregistered views can be rebound without changing the parameter list.
cacheable &= found != tensor_states_by_tensor_.end();
for (auto view = tensor->view_src; found == tensor_states_by_tensor_.end() && view != nullptr; view = view->view_src) {
found = tensor_states_by_tensor_.find(view);
}
if (found == tensor_states_by_tensor_.end()) {
LOG_ERROR("model manager tensor '%s' is not registered", ggml_get_name(tensor));
return false;
}
TensorState* state = found->second;
if ((compute_backend == nullptr || state->compute_backend == nullptr ||
state->compute_backend == compute_backend) &&
seen.insert(state).second) {
required_states.push_back(state);
if (seen.insert(state).second) {
states.push_back(state);
}
}
append_states(states);
if (cacheable && !tensors.empty()) {
static constexpr size_t MAX_RESOLVED_LISTS = 4;
resolved_tensor_states_.push_front({tensors, std::move(states)});
if (resolved_tensor_states_.size() > MAX_RESOLVED_LISTS) {
resolved_tensor_states_.pop_back();
}
}
return true;
@ -1274,8 +1306,7 @@ size_t ModelManager::compute_backend_alloc_size(const std::vector<TensorState*>&
size_t total_size = 0;
std::unordered_set<TensorState*> seen;
for (TensorState* state : states) {
if (state == nullptr || state->tensor == nullptr || !seen.insert(state).second ||
should_ignore(*state) || is_optional_missing_tensor(state->name)) {
if (state == nullptr || state->tensor == nullptr) {
continue;
}
const bool compute_resident =
@ -1285,6 +1316,9 @@ size_t ModelManager::compute_backend_alloc_size(const std::vector<TensorState*>&
if (missing_only && compute_resident) {
continue;
}
if (!seen.insert(state).second || should_ignore(*state) || is_optional_missing_tensor(state->name)) {
continue;
}
ggml_backend_buffer_type_t buffer_type = nullptr;
if (state->compute_backend == state->params_backend) {

View File

@ -2,6 +2,7 @@
#define __MODEL_MANAGER_H__
#include <cstdint>
#include <list>
#include <map>
#include <memory>
#include <set>
@ -84,9 +85,15 @@ private:
size_t resident_bytes = 0;
};
struct ResolvedTensorStates {
std::vector<ggml_tensor*> tensors;
std::vector<TensorState*> states;
};
ModelLoader model_loader_;
std::vector<std::unique_ptr<TensorState>> tensor_states_;
std::map<const ggml_tensor*, TensorState*> tensor_states_by_tensor_;
mutable std::list<ResolvedTensorStates> resolved_tensor_states_;
std::vector<std::unique_ptr<ParamsStorageBlock>> params_storage_blocks_;
std::vector<std::unique_ptr<ComputeStagingBlock>> compute_staging_blocks_;
std::map<ggml_backend_t, ggml_backend_buffer_type_t> split_buffer_types_;

View File

@ -857,14 +857,24 @@ bool StableDiffusionGGML::init_model_loader(ModelLoader& model_loader, ModelConf
}
bool StableDiffusionGGML::init(const sd_ctx_params_t* sd_ctx_params) {
#ifdef SD_USE_UPSTREAM_GGML
LOG_WARN(
"Using upstream GGML: FP8 and INT8 tensorwise/convrot are disabled. "
"Some operators may be unsupported and performance may be lower than with patched GGML.");
#endif
if (!validate_tensor_types(sd_ctx_params->wtype, sd_ctx_params->tensor_type_rules)) {
return false;
}
for (float scale : {sd_ctx_params->linear_scale, sd_ctx_params->attn_scale}) {
if (!std::isfinite(scale) || scale < 0.f || (scale > 0.f && !std::isfinite(1.f / scale))) {
LOG_ERROR("scale overrides must be finite positive values, or 0 to keep model defaults");
return false;
}
}
auto configuration = std::make_unique<ModelConfig>(*sd_ctx_params);
n_threads = sd_ctx_params->n_threads;
auto configuration = std::make_unique<ModelConfig>(*sd_ctx_params);
n_threads = sd_ctx_params->n_threads;
tensor_executor = std::make_unique<sd::ParallelExecutor>(n_threads > 0 ? n_threads : sd_get_num_physical_cores());
sd::ParallelScope tensor_scope(tensor_executor.get());
enable_mmap = sd_ctx_params->enable_mmap;
disable_prefetch = sd_ctx_params->disable_prefetch;
disable_segmented_compute = sd_ctx_params->disable_segmented_compute;
@ -2158,6 +2168,10 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
RunnerEndOnExit sample_control_runner_end{!control_image.empty() && control_net != nullptr ? control_net.get() : nullptr};
const bool apply_denoise_mask = !denoise_mask.empty() &&
std::any_of(denoise_mask.values().begin(), denoise_mask.values().end(),
[](float value) { return value != 1.f; });
std::vector<int> skip_layers(guidance.slg.layers, guidance.slg.layers + guidance.slg.layer_count);
float cfg_scale = guidance.txt_cfg;
float img_cfg_scale = guidance.img_cfg;
@ -2287,13 +2301,13 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
hunyuan_timestep_r_tensor = sd::Tensor<float>::from_vector({sigmas[step + 1]});
}
sd::Tensor<float> noised_input = x * c_in;
if (!denoise_mask.empty() && (version == VERSION_WAN2_2_TI2V || sd_version_is_ltxav(version) || sd_version_is_lingbot_video(version))) {
if (apply_denoise_mask && (version == VERSION_WAN2_2_TI2V || sd_version_is_ltxav(version) || sd_version_is_lingbot_video(version))) {
noised_input = noised_input * denoise_mask + sampling_init_latent * (1.0f - denoise_mask);
}
if (cache_runtime.spectrum_enabled && cache_runtime.spectrum.should_predict()) {
cache_runtime.spectrum.predict(&denoised);
if (!denoise_mask.empty()) {
if (apply_denoise_mask) {
denoised = denoised * denoise_mask + sampling_init_latent * (1.0f - denoise_mask);
}
if (preview_needed && sd_should_preview_denoised()) {
@ -2516,7 +2530,7 @@ sd::Tensor<float> StableDiffusionGGML::sample(const std::shared_ptr<DiffusionMod
if (cache_runtime.spectrum_enabled) {
cache_runtime.spectrum.update(denoised);
}
if (!denoise_mask.empty()) {
if (apply_denoise_mask) {
denoised = denoised * denoise_mask + sampling_init_latent * (1.0f - denoise_mask);
}
if (preview_needed && sd_should_preview_denoised()) {

View File

@ -8,6 +8,7 @@
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <vector>
@ -56,8 +57,9 @@ public:
std::shared_ptr<RNG> rng;
std::shared_ptr<RNG> sampler_rng = nullptr;
int n_threads = -1;
float default_flow_shift = INFINITY;
float active_flow_shift = INFINITY;
std::unique_ptr<sd::ParallelExecutor> tensor_executor;
float default_flow_shift = INFINITY;
float active_flow_shift = INFINITY;
std::shared_ptr<Conditioner> cond_stage_model;
std::shared_ptr<FrozenCLIPVisionEmbedder> clip_vision; // for svd or wan2.1 i2v
@ -206,6 +208,7 @@ public:
StableDiffusionGGML& sd;
std::unique_lock<std::recursive_mutex> lock;
bool acquired = false;
std::optional<sd::ParallelScope> tensor_scope;
explicit ContextOperation(StableDiffusionGGML& sd)
: sd(sd), lock(sd.execution_mutex, std::try_to_lock) {
@ -215,6 +218,7 @@ public:
}
sd.executing_ = true;
acquired = true;
tensor_scope.emplace(sd.tensor_executor.get());
}
~ContextOperation() {

View File

@ -584,10 +584,12 @@ namespace sd::model_builders {
}
if (sd_ctx_params->vae_conv_direct) {
LOG_INFO("Using Conv2d direct in the vae model");
LOG_INFO("Using Conv2d/Conv3d direct in the vae model");
result.vae->set_conv2d_direct_enabled(true);
result.vae->set_conv3d_direct_enabled(true);
if (result.preview) {
result.preview->set_conv2d_direct_enabled(true);
result.preview->set_conv3d_direct_enabled(true);
}
}
if (result.vae) {

View File

@ -26,13 +26,26 @@ static float get_cache_reuse_threshold(const sd_cache_params_t& params) {
}
const char* sd_type_name(enum sd_type_t type) {
if ((int)type < std::min<int>(SD_TYPE_COUNT, GGML_TYPE_COUNT)) {
return ggml_type_name((ggml_type)type);
if (type == SD_TYPE_F8_E4M3) {
return "f8_e4m3";
}
if (type == SD_TYPE_F8_E5M2) {
return "f8_e5m2";
}
const auto ggml_type = sd_type_to_ggml_type(type);
if (ggml_type != GGML_TYPE_COUNT) {
return ggml_type_name(ggml_type);
}
return NONE_STR;
}
enum sd_type_t str_to_sd_type(const char* str) {
if (!strcmp(str, "f8_e4m3")) {
return SD_TYPE_F8_E4M3;
}
if (!strcmp(str, "f8_e5m2")) {
return SD_TYPE_F8_E5M2;
}
for (int i = 0; i < std::min<int>(SD_TYPE_COUNT, GGML_TYPE_COUNT); i++) {
auto trait = ggml_get_type_traits((ggml_type)i);
if (!strcmp(str, trait->type_name)) {

View File

@ -14,6 +14,7 @@ UpscalerGGML::UpscalerGGML(int n_threads,
std::string backend_spec,
std::string params_backend_spec)
: n_threads(n_threads),
tensor_executor(n_threads > 0 ? n_threads : sd_get_num_physical_cores()),
direct(direct),
tile_size(tile_size),
backend_spec(std::move(backend_spec)),
@ -35,6 +36,7 @@ void UpscalerGGML::set_max_graph_vram_bytes(size_t max_vram_bytes) {
bool UpscalerGGML::load_from_file(const std::string& esrgan_path,
int n_threads) {
sd::ParallelScope tensor_scope(&tensor_executor);
ggml_log_set(sd_ggml_log_callback, nullptr);
std::string error;
@ -108,6 +110,7 @@ bool UpscalerGGML::load_from_file(const std::string& esrgan_path,
}
sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_tensor) {
sd::ParallelScope tensor_scope(&tensor_executor);
sd::Tensor<float> upscaled;
const int scale = esrgan_upscaler->config.scale;
if (tile_size <= 0 || (input_tensor.shape()[0] <= tile_size && input_tensor.shape()[1] <= tile_size)) {
@ -142,6 +145,7 @@ sd::Tensor<float> UpscalerGGML::upscale_tensor(const sd::Tensor<float>& input_te
}
sd_image_t UpscalerGGML::upscale(sd_image_t input_image, uint32_t upscale_factor) {
sd::ParallelScope tensor_scope(&tensor_executor);
// upscale_factor, unused for RealESRGAN_x4plus_anime_6B.pth
sd_image_t upscaled_image = {0, 0, 0, nullptr};
const int scale = esrgan_upscaler->config.scale;

View File

@ -17,6 +17,7 @@ struct UpscalerGGML {
std::shared_ptr<ESRGAN> esrgan_upscaler;
std::string esrgan_path;
int n_threads;
sd::ParallelExecutor tensor_executor;
bool direct = false;
int tile_size = 128;
size_t max_graph_vram_bytes = 0;