-
Notifications
You must be signed in to change notification settings - Fork 5
feat: dispatch MiniMax H3 ConvRot weights natively #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2026-08-11
Are you sure you want to change the base?
Changes from all commits
9bba810
669ce08
14a0cd0
edc8a59
135608e
4d7eec4
df88b48
1ff096d
3babd00
3d28264
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| +1 −0 | CMakeLists.txt | |
| +27 −0 | include/ggml.h | |
| +93 −0 | src/ggml-cpu/ggml-cpu.c | |
| +14 −0 | src/ggml-cpu/ggml-cpu.cpp | |
| +1 −0 | src/ggml-cpu/ops.h | |
| +256 −0 | src/ggml-cuda/convrot.cu | |
| +5 −0 | src/ggml-cuda/convrot.cuh | |
| +15 −0 | src/ggml-cuda/ggml-cuda.cu | |
| +18 −0 | src/ggml-metal/ggml-metal-device.cpp | |
| +1 −0 | src/ggml-metal/ggml-metal-device.h | |
| +11 −0 | src/ggml-metal/ggml-metal-device.m | |
| +21 −0 | src/ggml-metal/ggml-metal-impl.h | |
| +47 −0 | src/ggml-metal/ggml-metal-ops.cpp | |
| +1 −0 | src/ggml-metal/ggml-metal-ops.h | |
| +82 −0 | src/ggml-metal/ggml-metal.metal | |
| +6 −1 | src/ggml-rpc/ggml-rpc.cpp | |
| +73 −0 | src/ggml-vulkan/ggml-vulkan.cpp | |
| +67 −0 | src/ggml-vulkan/vulkan-shaders/mul_mat_convrot.comp | |
| +2 −0 | src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp | |
| +60 −2 | src/ggml.c | |
| +27 −0 | tests/CMakeLists.txt | |
| +151 −0 | tests/test-mul-mat-convrot-gpu.cpp | |
| +119 −0 | tests/test-mul-mat-convrot-metal.cpp | |
| +210 −0 | tests/test-mul-mat-convrot.cpp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,8 @@ | |
| #include <inttypes.h> | ||
| #include <stdarg.h> | ||
| #include <algorithm> | ||
| #include <atomic> | ||
| #include <cstdlib> | ||
| #include <atomic> | ||
| #include <cstring> | ||
| #include <fstream> | ||
| #include <functional> | ||
|
|
@@ -21,6 +21,7 @@ | |
| #include <set> | ||
| #include <sstream> | ||
| #include <string> | ||
| #include <stdexcept> | ||
| #include <unordered_map> | ||
| #include <unordered_set> | ||
| #include <vector> | ||
|
|
@@ -43,6 +44,81 @@ | |
|
|
||
| #define EPS 1e-05f | ||
|
|
||
| // Construct only the operation metadata needed for the normal backend | ||
| // supports_op query. This stays private to the loader policy: backend | ||
| // capabilities are expressed through the existing ggml interface, not a new | ||
| // public ConvRot-specific API. | ||
| inline bool ggml_backend_supports_convrot_op(ggml_backend_t backend) { | ||
| if (backend == nullptr) { | ||
| return false; | ||
| } | ||
| std::vector<uint8_t> storage(4 * ggml_tensor_overhead() + 1024); | ||
| ggml_init_params params = { | ||
| /*.mem_size =*/ storage.size(), | ||
| /*.mem_buffer =*/ storage.data(), | ||
| /*.no_alloc =*/ true, | ||
| }; | ||
| ggml_context* ctx = ggml_init(params); | ||
| if (ctx == nullptr) { | ||
| return false; | ||
| } | ||
| ggml_tensor* activations = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 256, 1); | ||
| ggml_tensor* weights = ggml_new_tensor_2d(ctx, GGML_TYPE_I8, 256, 1); | ||
| ggml_tensor* scales = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1); | ||
| ggml_tensor* op = ggml_mul_mat_convrot(ctx, activations, weights, scales, 256); | ||
| const bool supported = ggml_backend_supports_op(backend, op); | ||
| ggml_free(ctx); | ||
| return supported; | ||
| } | ||
|
|
||
| // Select the compact representation before any model parameter tensor is | ||
| // created. The default is deliberately native: an unsupported backend is a | ||
| // configuration error rather than a silent CPU reroute or full F16 expansion. | ||
| // Set SD_CONVROT_MODE=compat to explicitly request the compatibility loader. | ||
| inline String2TensorStorage select_convrot_tensor_storage(ggml_backend_t backend, | ||
| const String2TensorStorage& source, | ||
| const std::string& component, | ||
| const std::string& prefix = "") { | ||
| // OrderedMap's default copy also copies its iterator index; rebuild it so | ||
| // this independent policy view owns a valid index into its own list. | ||
| String2TensorStorage selected; | ||
| bool has_convrot = false; | ||
| for (const auto& [name, storage] : source) { | ||
| selected.insert({name, storage}); | ||
| has_convrot = has_convrot || | ||
| (name.rfind(prefix, 0) == 0 && storage.is_comfy_int8_convrot_weight()); | ||
| } | ||
| if (!has_convrot) { | ||
| return selected; | ||
| } | ||
|
|
||
| const char* mode = std::getenv("SD_CONVROT_MODE"); | ||
| const bool compatibility_mode = mode != nullptr && std::strcmp(mode, "compat") == 0; | ||
| if (mode != nullptr && !compatibility_mode && std::strcmp(mode, "native") != 0) { | ||
| throw std::runtime_error("invalid SD_CONVROT_MODE; expected 'native' or 'compat'"); | ||
| } | ||
| const char* backend_name = backend != nullptr ? ggml_backend_name(backend) : "unknown"; | ||
| if (compatibility_mode) { | ||
| LOG_INFO("ConvRot: using explicitly selected F16 compatibility path for %s on backend %s", | ||
| component.c_str(), backend_name); | ||
| return selected; | ||
| } | ||
| if (!ggml_backend_supports_convrot_op(backend)) { | ||
| throw std::runtime_error("ConvRot native support is required for " + component + | ||
| " but backend '" + backend_name + | ||
| "' lacks the 256-wide I8/F32 ConvRot operation; use a capable backend or set " | ||
| "SD_CONVROT_MODE=compat to select the F16 compatibility path"); | ||
| } | ||
| for (auto& [name, storage] : selected) { | ||
| if (name.rfind(prefix, 0) == 0 && storage.is_comfy_int8_convrot_weight()) { | ||
| storage.comfy_int8_native_enabled = true; | ||
| } | ||
| } | ||
| LOG_INFO("ConvRot: selected native compact I8/F32 path for %s on backend %s", | ||
| component.c_str(), backend_name); | ||
| return selected; | ||
| } | ||
|
|
||
| #ifndef __STATIC_INLINE__ | ||
| #define __STATIC_INLINE__ static inline | ||
| #endif | ||
|
|
@@ -1695,6 +1771,15 @@ struct WeightAdapter { | |
| ggml_tensor* b, | ||
| const std::string& prefix, | ||
| ForwardParams forward_params) = 0; | ||
| // Return only the adapter's output-space contribution. Native operations | ||
| // such as compact ConvRot own their base-weight arithmetic and therefore | ||
| // cannot use forward_with_lora() without recomputing an incompatible base. | ||
| virtual ggml_tensor* lora_output_delta(ggml_context* ctx, | ||
| ggml_backend_t backend, | ||
| ggml_tensor* x, | ||
| ggml_tensor* w, | ||
| const std::string& prefix, | ||
| ForwardParams forward_params) = 0; | ||
| virtual size_t get_extra_graph_size() = 0; | ||
| }; | ||
|
|
||
|
|
@@ -3875,12 +3960,31 @@ class Linear : public UnaryBlock { | |
| bool force_prec_f32; | ||
| bool allow_weight_scale; | ||
| bool has_weight_scale = false; | ||
| // This is distinct from `weight_scale`: the latter is a regular | ||
| // post-linear model parameter, while ConvRot's F32 vector is a private | ||
| // sidecar input to GGML_OP_MUL_MAT_CONVROT. | ||
| bool has_convrot_weight = false; | ||
| bool use_convrot_f16_compat = false; | ||
| float scale; | ||
| std::string prefix; | ||
|
|
||
| void init_params(ggml_context* ctx, const String2TensorStorage& tensor_storage_map = {}, const std::string prefix = "") override { | ||
| this->prefix = prefix; | ||
| has_weight_scale = false; | ||
| has_convrot_weight = false; | ||
| use_convrot_f16_compat = false; | ||
| const auto storage_it = tensor_storage_map.find(prefix + "weight"); | ||
| if (storage_it != tensor_storage_map.end() && storage_it->second.is_comfy_int8_convrot_weight() && | ||
| storage_it->second.comfy_int8_native_enabled) { | ||
| params["weight"] = ggml_new_tensor_2d(ctx, GGML_TYPE_I8, in_features, out_features); | ||
| params["weight.convrot_scale"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, out_features); | ||
| has_convrot_weight = true; | ||
| use_convrot_f16_compat = storage_it->second.name.rfind("text_encoders.llm.", 0) == 0; | ||
| if (bias) { | ||
| params["bias"] = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, out_features); | ||
| } | ||
| return; | ||
| } | ||
| enum ggml_type wtype = get_type(prefix + "weight", tensor_storage_map, GGML_TYPE_F32); | ||
| if (in_features % ggml_blck_size(wtype) != 0 || force_f32) { | ||
| wtype = GGML_TYPE_F32; | ||
|
|
@@ -3928,7 +4032,33 @@ class Linear : public UnaryBlock { | |
| } | ||
| ggml_tensor* linear_bias = has_weight_scale ? nullptr : b; | ||
| ggml_tensor* out = nullptr; | ||
| if (ctx->weight_adapter) { | ||
| if (has_convrot_weight) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: preserve or explicitly reject runtime LoRA on native ConvRot layers. This branch bypasses
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implemented in df88b48. Added a WeightAdapter output-delta API and MultiLoraAdapter implementation. Native ConvRot now adds LoRA output deltas after its compact base operation, preserving native ConvRot semantics without recomputing a mismatched base matmul. |
||
| // ConvRot weights and their tensor-wise scales remain compact at | ||
| // rest. The operator owns the scale semantics; do not route it | ||
| // through the ordinary `weight_scale` post-multiply path. | ||
| out = ggml_mul_mat_convrot(ctx->ggml_ctx, x, w, params["weight.convrot_scale"], 256); | ||
| // MiniMax H3's ConvRot text encoder is calibrated for the F16 | ||
| // compatibility arithmetic. CUDA reconstructs one F16 matrix at | ||
| // a time and uses its standard F16 GEMM without retaining an F16 | ||
| // copy of the complete text encoder. Other backends may ignore | ||
| // this hint and keep their native compact implementation. | ||
| if (use_convrot_f16_compat) { | ||
| ggml_mul_mat_convrot_set_f16_compat(out, true); | ||
| } | ||
| if (b != nullptr) { | ||
| out = ggml_add_inplace(ctx->ggml_ctx, out, b); | ||
| } | ||
| if (ctx->weight_adapter) { | ||
| WeightAdapter::ForwardParams forward_params; | ||
| forward_params.op_type = WeightAdapter::ForwardParams::op_type_t::OP_LINEAR; | ||
| forward_params.linear.force_prec_f32 = force_prec_f32; | ||
| forward_params.linear.scale = scale; | ||
| if (ggml_tensor* delta = ctx->weight_adapter->lora_output_delta( | ||
| ctx->ggml_ctx, ctx->backend, x, w, prefix, forward_params)) { | ||
| out = ggml_add_inplace(ctx->ggml_ctx, out, delta); | ||
| } | ||
| } | ||
| } else if (ctx->weight_adapter) { | ||
| WeightAdapter::ForwardParams forward_params; | ||
| forward_params.op_type = WeightAdapter::ForwardParams::op_type_t::OP_LINEAR; | ||
| forward_params.linear.force_prec_f32 = force_prec_f32; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Major: return configuration failures through the initialization error path. This exception, and the invalid-mode exception above, can propagate from runner construction through
StableDiffusionGGML::init()intonew_sd_ctx(), which does not catch them. Selecting an unsupported backend or mistypingSD_CONVROT_MODEcan therefore escape the C API and bypass its normal cleanup/null-result behavior. Please catch and translate these failures at initialization or use the existing error-return mechanism. This finding is based on tracing the constructor and C-entry-point paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented in df88b48. new_sd_ctx now catches std::exception and unknown exceptions from initialization, logs the failure, performs normal cleanup, and returns null through the C API.