#include "ggml-openvino.h" #include "ggml-backend-impl.h" #include "ggml-backend.h" #include "ggml-impl.h" #include "ggml-openvino-extra.h" #include "ggml-openvino/openvino/op_table.h" #include "ggml-openvino/utils.h" #include "ggml-quants.h" #include "ggml.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) # define WIN32_LEAN_AND_MEAN # ifndef NOMINMAX # define NOMINMAX # endif # include #else # include # include #endif // ===================================================== // OpenVINO Buffer Implementation using ov::Tensor // ===================================================== // // Design: This implementation uses a hybrid approach: // 1. For weight tensors: Store a pre-built ov::op::v0::Constant in tensor->extra // - This avoids the memcpy during graph construction // - For quantized weights, the constant is already converted to OpenVINO format // 2. For KV cache / compute tensors: Store an ov::Tensor in tensor->extra // - This can be directly passed to infer_request // - Future: can be changed to ov::RemoteTensor for GPU/NPU // // This design is similar to: // - CUDA split buffer: tensor->extra stores device pointers // - CPU repack buffer: tensor->extra stores tensor_traits with repacked data // ===================================================== // Buffer context that manages per-tensor allocations (no contiguous buffer for weights) struct ggml_backend_openvino_buffer_context { int device; std::string name; size_t id; // For non-weight buffers (KV cache, compute), we still use contiguous allocation void * data; size_t size; bool is_remote; // Wrapping of the buffer std::shared_ptr ov_buffer; // Track all extras for cleanup std::map tensor_extras; // Used for re-allocation on device for kvcache void * data_prev; ggml_backend_openvino_buffer_context(int device, size_t size, bool is_remote = false) : device(device), name(std::string(GGML_OPENVINO_NAME) + std::to_string(device)), id([]() { static std::atomic next_id{1}; return next_id.fetch_add(1); }()), data(nullptr), size(size), is_remote(is_remote) { if (size == 0) { return; } const auto & device_name = ggml_openvino_get_device_name(); if (is_remote) { GGML_ASSERT(device_name == "GPU"); auto remote_context = ggml_openvino_get_remote_context(); auto gpu_context = remote_context->as(); ov::intel_gpu::ocl::USMTensor usm_tensor = gpu_context.create_usm_device_tensor(ov::element::u8, ov::Shape{size}); data = usm_tensor.get(); ov_buffer = std::make_shared(std::move(usm_tensor)); } else { data = ggml_aligned_malloc(size); GGML_ASSERT(data); memset(data, 0, size); ov_buffer = std::make_shared(ov::element::u8, ov::Shape{size}, data); } if (data == nullptr) { GGML_LOG_ERROR("%s: failed to allocate %zu bytes\n", __func__, size); return; } if (reinterpret_cast(data) % TENSOR_ALIGNMENT != 0) { GGML_LOG_ERROR("%s: %s buffer is not aligned to %d bytes\n", __func__, device_name.c_str(), TENSOR_ALIGNMENT); GGML_ABORT("fatal error"); } } ~ggml_backend_openvino_buffer_context() { // Clean up all tensor extras // GGML_LOG_DEBUG("Deleting OpenVINO buffer context #%zu for device %d, size %zu MB\n", id, device, // size / 1024 / 1024); for (auto & pair : tensor_extras) { delete pair.second; } tensor_extras.clear(); if (!is_remote && data != nullptr) { ggml_aligned_free(data, size); } } }; // Buffer type context (per-device) struct ggml_backend_openvino_buffer_type_context { int device; std::string name; }; // ===================================================== // Host weight-buffer release (GGML_OPENVINO_RELEASE_WEIGHTS) // ===================================================== // The OpenVINO weight Constants are zero-copy views into the host buffers // allocated here (ggml_aligned_malloc, anonymous memory). On GPU the plugin // holds its own device copy after compile_model, so the host pages are dead // weight for inference and can be dropped to reclaim RSS (~weights size). // // We do NOT free the buffer (ggml owns its lifetime and tensors still point // into it); instead madvise(MADV_DONTNEED) drops the resident pages while // keeping the mapping valid. A later recompile would re-read these Constants // from now-zeroed memory and produce garbage, so once released we fail fast // if the cache-miss compile branch is reached again (see utils.cpp). namespace { struct ov_weight_buffer_registry { std::mutex mutex; // (data, size) of every non-remote weight buffer, for madvise. std::vector> buffers; bool released = false; }; ov_weight_buffer_registry & ov_weight_registry() { static ov_weight_buffer_registry reg; return reg; } } // namespace void ggml_openvino_register_weight_buffer(void * data, size_t size) { if (data == nullptr || size == 0) { return; } auto & reg = ov_weight_registry(); std::lock_guard lock(reg.mutex); for (const auto & b : reg.buffers) { if (b.first == data) { return; // already registered } } reg.buffers.emplace_back(data, size); } bool ggml_openvino_weight_buffers_released() { auto & reg = ov_weight_registry(); std::lock_guard lock(reg.mutex); return reg.released; } void ggml_openvino_release_weight_buffers() { auto & reg = ov_weight_registry(); std::lock_guard lock(reg.mutex); if (reg.released) { return; } size_t total = 0; #if !defined(_WIN32) for (const auto & b : reg.buffers) { // Align down/up to page boundaries so madvise only drops whole pages // fully owned by this buffer. const long page = sysconf(_SC_PAGESIZE); uintptr_t start = reinterpret_cast(b.first); uintptr_t end = start + b.second; uintptr_t astart = (start + page - 1) & ~(uintptr_t) (page - 1); uintptr_t aend = end & ~(uintptr_t) (page - 1); if (aend > astart) { if (madvise(reinterpret_cast(astart), aend - astart, MADV_DONTNEED) == 0) { total += aend - astart; } } } #endif reg.released = true; GGML_LOG_INFO("%s: released %zu MB of host weight buffers (%zu buffers)\n", __func__, total / 1024 / 1024, reg.buffers.size()); } // Buffer interface functions static void ggml_backend_openvino_buffer_free_buffer(ggml_backend_buffer_t buffer) { ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; delete ctx; } static void * ggml_backend_openvino_buffer_get_base(ggml_backend_buffer_t buffer) { ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; return ctx->data; } static bool is_stateful_enabled() { return ggml_openvino_getenv_int("GGML_OPENVINO_STATEFUL_EXECUTION") != 0; } static enum ggml_status ggml_backend_openvino_buffer_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) { // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name); ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; // Put kvcache on device memory for GPU (NPU memory is too small even for kvcache) if (strncmp(tensor->name, "cache_", 6) == 0 && !ctx->is_remote && ggml_openvino_get_device_name() == "GPU" && !is_stateful_enabled()) { GGML_ASSERT(ctx->tensor_extras.empty()); auto device = ctx->device; auto size = ctx->size; auto * data_prev = ctx->data; delete ctx; ctx = new ggml_backend_openvino_buffer_context(device, size, true); buffer->context = ctx; tensor->data = (char *) ctx->data + ((char *) tensor->data - (char *) data_prev); } // Views share the extra from view_src if (tensor->view_src != nullptr) { GGML_ASSERT(tensor->view_src->buffer->buft == buffer->buft); if (tensor->view_src->extra != nullptr) { tensor->extra = tensor->view_src->extra; } return GGML_STATUS_SUCCESS; } ctx = (ggml_backend_openvino_buffer_context *) buffer->context; if (tensor->data != nullptr && !ggml_is_quantized(tensor->type)) { ggml_openvino_tensor_extra * extra = ggml_openvino_create_tensor_extra(tensor, ctx->is_remote); if (extra != nullptr) { auto it = ctx->tensor_extras.find(tensor); if (it != ctx->tensor_extras.end()) { delete it->second; } ctx->tensor_extras[tensor] = extra; tensor->extra = extra; } } return GGML_STATUS_SUCCESS; } static void ggml_backend_openvino_buffer_memset_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, uint8_t value, size_t offset, size_t size) { // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name); GGML_ASSERT(tensor != nullptr && tensor->data != nullptr); ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; if (ctx->is_remote) { // For remote (device) buffers, use OpenCL USM memfill cl_command_queue queue = ggml_openvino_get_cl_queue(); auto mem_fill_fn = ggml_openvino_get_clEnqueueMemFillINTEL(); if (queue != nullptr && mem_fill_fn != nullptr) { uint8_t pattern = value; cl_int err = mem_fill_fn(queue, (char *) tensor->data + offset, &pattern, sizeof(pattern), size, 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_ERROR("%s: clEnqueueMemFillINTEL failed with error %d\n", __func__, err); } clFinish(queue); } else { GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemFillINTEL not available for GPU buffer\n", __func__); } } else { memset((char *) tensor->data + offset, value, size); } } static void ggml_backend_openvino_buffer_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, const void * data, size_t offset, size_t size) { // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name); GGML_ASSERT(tensor != nullptr && tensor->data != nullptr); ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; // Check if this is a weight buffer (usage is set BEFORE set_tensor is called, except in test-backend-ops) bool is_weight_buffer = (buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS); // Full tensor set: offset=0, full size, not a view bool is_full_tensor_set = (offset == 0 && size == ggml_nbytes(tensor) && tensor->view_src == nullptr); // 2D tensor (typical weight shape), or a 3D quantized MoE expert weight (MUL_MAT_ID). Dense 3D // expert weights are handled later in create_weight_node instead. bool is_2d = (tensor->ne[2] == 1 && tensor->ne[3] == 1); bool is_supported_weight_shape = is_2d || (tensor->ne[3] == 1 && ggml_is_quantized(tensor->type)); if (is_weight_buffer && is_full_tensor_set && is_supported_weight_shape) { try { auto result = process_weight_tensor(tensor, data, tensor->data); result.weight_node->set_friendly_name(tensor->name); // const auto & layout = result.layout; ggml_openvino_extra_base * extra; // Quantized path with extracted weight/scale/zp tensors if (result.is_quantized()) { extra = new ggml_openvino_quantized_weight_extra(std::move(result.weights), std::move(result.scales), std::move(result.zp), result.weight_node); // if (layout.is_requant) { // GGML_LOG_DEBUG("%s: requantized %s to %s (u%d, block_size=%ld)\n", __func__, tensor->name, // extra_quant_type_name(layout.requant_type.value()), layout.is_u4 ? 4 : 8, // layout.weights_per_block); // } else { // int64_t n_blocks = ggml_nelements(tensor) / layout.weights_per_block; // GGML_LOG_DEBUG("%s: extracted quantized weight node for %s (u%d, %zu weights, %ld blocks)\n", // __func__, tensor->name, layout.is_u4 ? 4 : 8, layout.weights_size, n_blocks); // } } else { // F16/F32/BF16 weight or F16-requant extra = new ggml_openvino_weight_extra(std::move(result.weights), result.weight_node); // if (layout.total_size > 0) { // GGML_LOG_DEBUG("%s: requantized %s to F16\n", __func__, tensor->name); // } else { // GGML_LOG_DEBUG("%s: created shared-memory weight node for %s\n", __func__, tensor->name); // } } ctx->tensor_extras[tensor] = extra; tensor->extra = extra; // Register the host buffer so its pages can be dropped after the GPU // plugin has its own device copy (GGML_OPENVINO_RELEASE_WEIGHTS). if (!ctx->is_remote) { // Weights are set once at model load. Setting a weight after a release // means a second model is loading while the first's compiled graph is // pinned — that graph would be wrongly reused with this model's key. // Fail loud rather than return silently-wrong results. if (ggml_openvino_weight_buffers_released()) { GGML_ABORT( "ggml-openvino: loading a new model while GGML_OPENVINO_RELEASE_WEIGHTS pinned a previous " "model's compiled graph. This mode supports a single model per process; unset it for " "multi-model runs."); } ggml_openvino_register_weight_buffer(ctx->data, ctx->size); } } catch (const std::exception & e) { GGML_LOG_ERROR("%s: failed to process weight tensor for %s: %s\n", __func__, tensor->name, e.what()); memcpy((char *) tensor->data + offset, data, size); } } else { // Non-weight tensor (KV cache, activations, etc.) - copy data. test-backend-ops also goes here if (ctx->is_remote) { cl_command_queue queue = ggml_openvino_get_cl_queue(); auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL(); if (queue != nullptr && mem_cpy_fn != nullptr) { cl_int err = mem_cpy_fn(queue, CL_TRUE, (char *) tensor->data + offset, data, size, 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL failed with error %d\n", __func__, err); } } else { GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__); } } else { memcpy((char *) tensor->data + offset, data, size); } ggml_openvino_tensor_extra * extra = ggml_openvino_create_tensor_extra(tensor, ctx->is_remote); if (extra == nullptr) { // GGML_LOG_ERROR("%s: failed to create tensor extra for %s\n", __func__, tensor->name); return; } auto it = ctx->tensor_extras.find(tensor); if (it != ctx->tensor_extras.end()) { delete it->second; } ctx->tensor_extras[tensor] = extra; tensor->extra = extra; } } static void ggml_backend_openvino_buffer_get_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * tensor, void * data, size_t offset, size_t size) { // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name); GGML_ASSERT(tensor != nullptr && tensor->data != nullptr); ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; if (ctx->is_remote) { // For remote (device) buffers, use OpenCL USM memcpy (device-to-host) cl_command_queue queue = ggml_openvino_get_cl_queue(); auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL(); if (queue != nullptr && mem_cpy_fn != nullptr) { cl_int err = mem_cpy_fn(queue, CL_TRUE, data, (const char *) tensor->data + offset, size, 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL failed with error %d\n", __func__, err); } } else { GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__); } } else { memcpy(data, (const char *) tensor->data + offset, size); } } static bool ggml_backend_openvino_buffer_cpy_tensor(ggml_backend_buffer_t buffer, const ggml_tensor * src, ggml_tensor * dst) { // GGML_LOG_DEBUG("%s: src tensor name=%s, dst tensor name=%s\n", __func__, src->name, dst->name); GGML_ASSERT(src != nullptr && dst != nullptr); ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; if (ctx->is_remote) { // For remote (device) buffers, use OpenCL USM memcpy cl_command_queue queue = ggml_openvino_get_cl_queue(); auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL(); if (queue == nullptr || mem_cpy_fn == nullptr) { GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__); return false; } // Can copy from host to device if (ggml_backend_buffer_is_host(src->buffer)) { cl_int err = mem_cpy_fn(queue, CL_TRUE, dst->data, src->data, ggml_nbytes(src), 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL (host-to-device) failed with error %d\n", __func__, err); return false; } return true; } // Can also copy from device to device if both are OpenVINO remote buffers if (ggml_backend_buffer_is_openvino(src->buffer)) { ggml_backend_openvino_buffer_context * src_ctx = (ggml_backend_openvino_buffer_context *) src->buffer->context; if (src_ctx->is_remote) { cl_int err = mem_cpy_fn(queue, CL_TRUE, dst->data, src->data, ggml_nbytes(src), 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL (device-to-device) failed with error %d\n", __func__, err); return false; } return true; } } return false; } // Host buffer - can copy from any host buffer if (ggml_backend_buffer_is_host(src->buffer)) { memcpy(dst->data, src->data, ggml_nbytes(src)); return true; } return false; } static void ggml_backend_openvino_buffer_clear(ggml_backend_buffer_t buffer, uint8_t value) { ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; GGML_ASSERT(ctx->data != nullptr); if (ctx->is_remote) { cl_command_queue queue = ggml_openvino_get_cl_queue(); auto mem_fill_fn = ggml_openvino_get_clEnqueueMemFillINTEL(); if (queue != nullptr && mem_fill_fn != nullptr) { uint8_t pattern = value; cl_int err = mem_fill_fn(queue, ctx->data, &pattern, sizeof(pattern), ctx->size, 0, nullptr, nullptr); if (err != CL_SUCCESS) { GGML_LOG_WARN("%s: clEnqueueMemFillINTEL failed with error %d\n", __func__, err); } clFinish(queue); } else { GGML_LOG_WARN("%s: no OpenCL queue or clEnqueueMemFillINTEL not available for GPU buffer clear\n", __func__); } } else { memset(ctx->data, value, ctx->size); } } static const ggml_backend_buffer_i ggml_backend_openvino_buffer_interface = { /* .free_buffer = */ ggml_backend_openvino_buffer_free_buffer, /* .get_base = */ ggml_backend_openvino_buffer_get_base, /* .init_tensor = */ ggml_backend_openvino_buffer_init_tensor, /* .memset_tensor = */ ggml_backend_openvino_buffer_memset_tensor, /* .set_tensor = */ ggml_backend_openvino_buffer_set_tensor, /* .get_tensor = */ ggml_backend_openvino_buffer_get_tensor, /* .set_tensor_2d = */ NULL, /* .get_tensor_2d = */ NULL, /* .cpy_tensor = */ ggml_backend_openvino_buffer_cpy_tensor, /* .clear = */ ggml_backend_openvino_buffer_clear, /* .reset = */ NULL, }; // Buffer type interface functions static const char * ggml_backend_openvino_buffer_type_get_name(ggml_backend_buffer_type_t buft) { ggml_backend_openvino_buffer_type_context * ctx = (ggml_backend_openvino_buffer_type_context *) buft->context; return ctx->name.c_str(); } static ggml_backend_buffer_t ggml_backend_openvino_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) { ggml_backend_openvino_buffer_type_context * buft_ctx = (ggml_backend_openvino_buffer_type_context *) buft->context; // Create buffer context with contiguous memory allocation ggml_backend_openvino_buffer_context * ctx = new ggml_backend_openvino_buffer_context(buft_ctx->device, size); if (ctx->data == nullptr && size > 0) { GGML_LOG_ERROR("%s: failed to allocate buffer of size %zu\n", __func__, size); delete ctx; return nullptr; } return ggml_backend_buffer_init(buft, ggml_backend_openvino_buffer_interface, ctx, size); } static size_t ggml_backend_openvino_buffer_type_get_alignment(ggml_backend_buffer_type_t buft) { GGML_UNUSED(buft); return TENSOR_ALIGNMENT; } static size_t ggml_backend_openvino_buffer_type_get_max_size(ggml_backend_buffer_type_t buft) { GGML_UNUSED(buft); return SIZE_MAX; } static size_t ggml_backend_openvino_buffer_type_get_alloc_size(ggml_backend_buffer_type_t buft, const ggml_tensor * tensor) { GGML_UNUSED(buft); // For quantized weight tensors, we need extra space for extracted data. if (ggml_is_quantized(tensor->type) && tensor->ne[3] == 1) { ggml_openvino_extracted_layout layout = ggml_openvino_get_extracted_layout(tensor); if (layout.total_size > 0) { // GGML_LOG_DEBUG("%s: tensor %s needs %zu bytes (original %zu, extracted: weights=%zu scales=%zu zp=%zu)\n", // __func__, tensor->name, layout.total_size, ggml_nbytes(tensor), layout.weights_size, // layout.scales_size, layout.zp_size); return layout.total_size; } } return ggml_nbytes(tensor); } static const ggml_backend_buffer_type_i ggml_backend_openvino_buffer_type_interface = { /* .get_name = */ ggml_backend_openvino_buffer_type_get_name, /* .alloc_buffer = */ ggml_backend_openvino_buffer_type_alloc_buffer, /* .get_alignment = */ ggml_backend_openvino_buffer_type_get_alignment, /* .get_max_size = */ ggml_backend_openvino_buffer_type_get_max_size, /* .get_alloc_size = */ ggml_backend_openvino_buffer_type_get_alloc_size, /* .is_host = */ nullptr, }; // Get buffer type for a specific device GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_buffer_type(int device) { GGML_ASSERT(device >= 0 && device < ggml_backend_openvino_get_device_count()); static std::mutex mutex; std::lock_guard lock(mutex); static std::vector buffer_types; static std::vector buffer_type_contexts; if (buffer_types.empty()) { int device_count = ggml_backend_openvino_get_device_count(); buffer_types.resize(device_count); buffer_type_contexts.resize(device_count); for (int i = 0; i < device_count; i++) { buffer_type_contexts[i].device = i; buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i); buffer_types[i] = ggml_backend_buffer_type{ /* .iface = */ ggml_backend_openvino_buffer_type_interface, /* .device = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), i), /* .context = */ &buffer_type_contexts[i], }; } } return &buffer_types[device]; } // ===================================================== // OpenVINO Host Buffer Implementation // ===================================================== static const char * ggml_backend_openvino_host_buffer_type_get_name(ggml_backend_buffer_type_t buft) { ggml_backend_openvino_buffer_type_context * ctx = (ggml_backend_openvino_buffer_type_context *) buft->context; static std::string name; name = ctx->name + "_HOST"; return name.c_str(); } static bool ggml_backend_openvino_host_buffer_type_is_host(ggml_backend_buffer_type_t buft) { GGML_UNUSED(buft); return true; } static const ggml_backend_buffer_type_i ggml_backend_openvino_host_buffer_type_interface = { /* .get_name = */ ggml_backend_openvino_host_buffer_type_get_name, /* .alloc_buffer = */ ggml_backend_openvino_buffer_type_alloc_buffer, /* .get_alignment = */ ggml_backend_openvino_buffer_type_get_alignment, /* .get_max_size = */ ggml_backend_openvino_buffer_type_get_max_size, /* .get_alloc_size = */ ggml_backend_openvino_buffer_type_get_alloc_size, /* .is_host = */ ggml_backend_openvino_host_buffer_type_is_host, }; GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_host_buffer_type(int device) { GGML_ASSERT(device >= 0 && device < ggml_backend_openvino_get_device_count()); static std::mutex mutex; std::lock_guard lock(mutex); static std::vector buffer_types; static std::vector buffer_type_contexts; if (buffer_types.empty()) { int device_count = ggml_backend_openvino_get_device_count(); buffer_types.resize(device_count); buffer_type_contexts.resize(device_count); for (int i = 0; i < device_count; i++) { buffer_type_contexts[i].device = i; buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i); buffer_types[i] = ggml_backend_buffer_type{ /* .iface = */ ggml_backend_openvino_host_buffer_type_interface, /* .device = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), i), /* .context = */ &buffer_type_contexts[i], }; } } return &buffer_types[device]; } bool ggml_backend_buffer_is_openvino(ggml_backend_buffer_t buffer) { return buffer->iface.free_buffer == ggml_backend_openvino_buffer_free_buffer; } size_t ggml_backend_openvino_buffer_get_ctx_id(ggml_backend_buffer_t buffer) { if (!ggml_backend_buffer_is_openvino(buffer)) { return 0; } ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context; return ctx->id; } bool ggml_openvino_buffer_is_remote(const ggml_tensor * tensor) { if (tensor == nullptr || tensor->buffer == nullptr) { return false; } if (!ggml_backend_buffer_is_openvino(tensor->buffer)) { return false; } auto * ctx = static_cast(tensor->buffer->context); return ctx->is_remote; } void ggml_openvino_buffer_register_extra(ggml_tensor * tensor, ggml_openvino_extra_base * extra) { GGML_ASSERT(tensor != nullptr); GGML_ASSERT(tensor->buffer != nullptr); GGML_ASSERT(ggml_backend_buffer_is_openvino(tensor->buffer)); auto * ctx = static_cast(tensor->buffer->context); auto it = ctx->tensor_extras.find(tensor); if (it != ctx->tensor_extras.end()) { delete it->second; } ctx->tensor_extras[tensor] = extra; tensor->extra = extra; } bool ggml_backend_buft_is_openvino(ggml_backend_buffer_type_t buft) { return buft->iface.get_name == ggml_backend_openvino_buffer_type_get_name; } bool ggml_backend_buft_is_openvino_host(ggml_backend_buffer_type_t buft) { return buft->iface.get_name == ggml_backend_openvino_host_buffer_type_get_name; } static void ggml_backend_openvino_free(ggml_backend_t backend) { ggml_backend_openvino_context * ctx = (ggml_backend_openvino_context *) backend->context; if (ctx->runtime_context) { auto r_ctx = std::static_pointer_cast(ctx->runtime_context); if (--r_ctx->backend_count == 0) { // If host weight buffers were released (GGML_OPENVINO_RELEASE_WEIGHTS), the // dropped pages can never be repopulated, so a recompile is impossible. Keep // the compiled-model cache alive across backend teardown so the next context // reuses it instead of recompiling against zeroed weights. if (!ggml_openvino_weight_buffers_released()) { r_ctx->clear_caches(); } } } delete ctx; delete backend; } static const char * ggml_backend_openvino_get_name(ggml_backend_t backend) { return GGML_OPENVINO_NAME; GGML_UNUSED(backend); } static enum ggml_status ggml_backend_openvino_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) { return ov_graph_compute(cgraph, backend); GGML_UNUSED(backend); } static const ggml_backend_i ggml_backend_openvino_interface = { /* .get_name = */ ggml_backend_openvino_get_name, /* .free = */ ggml_backend_openvino_free, /* .set_tensor_async = */ NULL, /* .get_tensor_async = */ NULL, /* .set_tensor_2d_async = */ NULL, /* .get_tensor_2d_async = */ NULL, /* .cpy_tensor_async = */ NULL, /* .synchronize = */ NULL, /* .graph_plan_create = */ NULL, /* .graph_plan_free = */ NULL, /* .graph_plan_update = */ NULL, /* .graph_plan_compute = */ NULL, /* .graph_compute = */ ggml_backend_openvino_graph_compute, /* .event_record = */ NULL, /* .event_wait = */ NULL, /* .graph_optimize = */ NULL, }; int ggml_backend_openvino_get_device_count() { return 1; } static ggml_guid_t ggml_backend_openvino_guid(void) { static ggml_guid guid = {0x12, 0xa8, 0xae, 0xf4, 0xc0, 0x1e, 0x61, 0x97, 0x8f, 0xeb, 0x33, 0x04, 0xa1, 0x33, 0x51, 0x2d}; return &guid; } static std::shared_ptr get_ov_runtime_context_ptr() { static std::shared_ptr r_ctx = [] { auto ctx = std::make_shared(); ctx->device = ggml_openvino_get_device_name(); ctx->stateful = is_stateful_enabled() && !ggml_openvino_is_npu(); return ctx; }(); return r_ctx; } // backend API GGML_BACKEND_API ggml_backend_t ggml_backend_openvino_init(int device) { if (device < 0 || device >= ggml_backend_openvino_get_device_count()) { GGML_LOG_ERROR("%s: invalid device %d\n", __func__, device); return nullptr; } ggml_backend_openvino_context * ctx = new ggml_backend_openvino_context; if (ctx == nullptr) { GGML_LOG_ERROR("%s: failed to allocate context\n", __func__); return nullptr; } ctx->runtime_context = get_ov_runtime_context_ptr(); if (ctx->runtime_context == nullptr) { GGML_LOG_ERROR("%s: failed to allocate runtime context\n", __func__); delete ctx; return nullptr; } std::shared_ptr r_ctx = std::static_pointer_cast(ctx->runtime_context); r_ctx->backend_count++; ggml_backend_t openvino_backend = new ggml_backend{ /* .guid = */ ggml_backend_openvino_guid(), /* .interface = */ ggml_backend_openvino_interface, /* .device = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), device), /* .context = */ ctx, }; return openvino_backend; } GGML_BACKEND_API bool ggml_backend_is_openvino(ggml_backend_t backend) { return backend != NULL && ggml_guid_matches(backend->guid, ggml_backend_openvino_guid()); } struct ggml_backend_openvino_device_context { int device; std::string name; std::string description; }; static const char * ggml_backend_openvino_device_get_name(ggml_backend_dev_t dev) { ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context; return ctx->name.c_str(); } static const char * ggml_backend_openvino_device_get_description(ggml_backend_dev_t dev) { ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context; return ctx->description.c_str(); } static void ggml_backend_openvino_device_get_memory(ggml_backend_dev_t dev, size_t * free, size_t * total) { #ifdef _WIN32 MEMORYSTATUSEX status; status.dwLength = sizeof(status); GlobalMemoryStatusEx(&status); *total = status.ullTotalPhys; *free = status.ullAvailPhys; #else long pages = sysconf(_SC_PHYS_PAGES); long page_size = sysconf(_SC_PAGE_SIZE); *total = pages * page_size; // "free" system memory is ill-defined, for practical purposes assume that all of it is free: *free = *total; #endif // _WIN32 GGML_UNUSED(dev); } static enum ggml_backend_dev_type ggml_backend_openvino_device_get_type(ggml_backend_dev_t dev) { GGML_UNUSED(dev); return GGML_BACKEND_DEVICE_TYPE_GPU; } static void ggml_backend_openvino_device_get_props(ggml_backend_dev_t dev, ggml_backend_dev_props * props) { props->name = ggml_backend_openvino_device_get_name(dev); props->description = ggml_backend_openvino_device_get_description(dev); props->type = ggml_backend_openvino_device_get_type(dev); ggml_backend_openvino_device_get_memory(dev, &props->memory_free, &props->memory_total); props->caps = { /* .async = */ false, /* .host_buffer = */ false, /* .buffer_from_host_ptr = */ false, /* .events = */ false, /* .mmap_support = */ true, }; } static ggml_backend_t ggml_backend_openvino_device_init(ggml_backend_dev_t dev, const char * params) { GGML_UNUSED(params); ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context; return ggml_backend_openvino_init(ctx->device); } static ggml_backend_buffer_type_t ggml_backend_openvino_device_get_buffer_type(ggml_backend_dev_t dev) { ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context; return ggml_backend_openvino_buffer_type(ctx->device); } static ggml_backend_buffer_type_t ggml_backend_openvino_device_get_host_buffer_type(ggml_backend_dev_t dev) { ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context; return ggml_backend_openvino_host_buffer_type(ctx->device); } static bool has_view_op_input(const ggml_tensor * op) { for (int i = 0; i < GGML_MAX_SRC; i++) { if (op->src[i] == nullptr) { break; } if (op->src[i]->op == GGML_OP_VIEW) { return true; } } return false; } static bool has_non_contiguous_view_input(const ggml_tensor * op) { for (int i = 0; i < GGML_MAX_SRC; i++) { if (op->src[i] == nullptr) { break; } if (op->src[i]->op == GGML_OP_VIEW && !ggml_is_contiguous(op->src[i])) { return true; } } return false; } static bool is_supported_flash_attn_pattern(const ggml_tensor * op) { // Each Q/K/V input must follow one of: // PERMUTE -> VIEW -> base (view_src==nullptr) (llama KV-cache path) // PERMUTE -> RESHAPE -> base (view_src==nullptr) (whisper Q) // VIEW -> base (view_src==nullptr) (whisper K/V from kv_pad) for (int i = 0; i < 3; i++) { const ggml_tensor * src = op->src[i]; if (src->op == GGML_OP_PERMUTE) { if (src->src[0] == nullptr) { return false; } if (src->src[0]->op != GGML_OP_VIEW && src->src[0]->op != GGML_OP_RESHAPE) { return false; } if (src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) { return false; } } else if (src->op == GGML_OP_VIEW) { if (src->src[0] == nullptr || src->src[0]->view_src != nullptr) { return false; } } else { return false; } } return true; } static bool is_gemma3n_flash_attn_pattern(const ggml_tensor * op) { if (!is_supported_flash_attn_pattern(op)) { return false; } const ggml_tensor * q_base = op->src[0] != nullptr && op->src[0]->src[0] != nullptr ? op->src[0]->src[0]->src[0] : nullptr; const ggml_tensor * k_base = op->src[1] != nullptr && op->src[1]->src[0] != nullptr ? op->src[1]->src[0]->src[0] : nullptr; const ggml_tensor * v_base = op->src[2] != nullptr && op->src[2]->src[0] != nullptr ? op->src[2]->src[0]->src[0] : nullptr; if (q_base == nullptr || q_base->op != GGML_OP_ROPE) { return false; } // gemma3n direct attention path (no KV cache): q=ROPE, k=ROPE, v=RMS_NORM // Only match this specific pattern to avoid falsely catching other models // (e.g. Gemma4) that also use scale=1.0 with KV-cache backed attention. const bool is_qkv_direct = k_base != nullptr && v_base != nullptr && k_base->op == GGML_OP_ROPE && v_base->op == GGML_OP_RMS_NORM; return is_qkv_direct; } static bool checked_mul_size(size_t a, size_t b, size_t & out) { if (a == 0 || b == 0) { out = 0; return true; } if (a > SIZE_MAX / b) { return false; } out = a * b; return true; } static bool tensor_view_fits_src_buffer(const ggml_tensor * tensor) { if (tensor->view_src == nullptr) { return true; } const size_t src_nbytes = ggml_nbytes(tensor->view_src); if (tensor->view_offs > src_nbytes) { return false; } const size_t tensor_nbytes = ggml_nbytes(tensor); return tensor_nbytes <= src_nbytes - tensor->view_offs; } static bool cpy_output_view_is_supported(const ggml_tensor * op) { if (op->view_src == nullptr) { return true; } if (!tensor_view_fits_src_buffer(op)) { return false; } return ggml_nbytes(op) == 0 || ggml_is_contiguous(op); } static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) { const ggml_tensor * as = op->src[0]; const ggml_tensor * ids = op->src[2]; if (as == nullptr || ids == nullptr) { return true; } // The MXFP4 MUL_MAT_ID translation (translate_mul_mat_id_mxfp4_packed in mul_mat_id.cpp) // materializes selected expert weights with shape [n_tokens, n_used, rows, k]. Skip cases that // would create a very large temporary and let the scheduler fall back instead. Every other weight // type goes through GatherMatmul, which never materializes this temporary. size_t tmp_elems = 1; if (!checked_mul_size(tmp_elems, static_cast(ids->ne[1]), tmp_elems) || !checked_mul_size(tmp_elems, static_cast(ids->ne[0]), tmp_elems) || !checked_mul_size(tmp_elems, static_cast(as->ne[1]), tmp_elems) || !checked_mul_size(tmp_elems, static_cast(as->ne[0]), tmp_elems)) { return true; } size_t tmp_bytes = 0; if (!checked_mul_size(tmp_elems, sizeof(float), tmp_bytes)) { return true; } static constexpr size_t mul_mat_id_tmp_limit = 1ULL << 30; // 1 GiB return tmp_bytes > mul_mat_id_tmp_limit; } static bool tensor_name_starts_with(const ggml_tensor * tensor, const char * prefix) { return tensor != nullptr && strncmp(tensor->name, prefix, strlen(prefix)) == 0; } static bool is_msa_block_mask_expansion(const ggml_tensor * op) { if (tensor_name_starts_with(op, "msa_")) { return true; } const ggml_tensor * src = op->src[0]; while (src != nullptr && (src->op == GGML_OP_RESHAPE || src->op == GGML_OP_REPEAT)) { if (tensor_name_starts_with(src, "msa_block_mask")) { return true; } src = src->src[0]; } return tensor_name_starts_with(src, "msa_block_mask"); } namespace { struct ggml_openvino_op_support { bool is_supported = true; std::string reason; operator bool() const { return is_supported; } }; } // namespace static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) { if (is_msa_block_mask_expansion(op)) { return {false, "MSA block mask expansion is not supported"}; } switch (op->op) { case GGML_OP_CONCAT: { if (op->type == GGML_TYPE_I64) { return {false, "CONCAT with I64 type is not supported"}; } if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16 && has_view_op_input(op)) { return {false, "CONCAT with BF16 type and VIEW input is not supported on GPU"}; } break; } case GGML_OP_SET: { const auto nb1 = static_cast(op->op_params[0]); const auto nb2 = static_cast(op->op_params[1]); const auto nb3 = static_cast(op->op_params[2]); // OpenVINO SET translation currently supports dst layouts that match src0 strides. if (op->src[0] == nullptr || nb1 != op->src[0]->nb[1] || nb2 != op->src[0]->nb[2] || nb3 != op->src[0]->nb[3]) { return {false, "SET op with dst nb1=" + std::to_string(nb1) + ", nb2=" + std::to_string(nb2) + ", nb3=" + std::to_string(nb3) + " that does not match src0 strides nb[1]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[1]) : "null") + ", nb[2]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[2]) : "null") + ", nb[3]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[3]) : "null")}; } break; } case GGML_OP_GET_ROWS: case GGML_OP_SET_ROWS: { if (op->ne[3] != 1) { return {false, "GET_ROWS/SET_ROWS with ne[3] != 1 (ne[3]=" + std::to_string(op->ne[3]) + ") is not supported"}; } if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" && op->src[0]->type == GGML_TYPE_BF16) { return {false, "GET_ROWS with BF16 src0 is not supported on GPU"}; } if (op->ne[0] == 256 && (op->src[0]->type == GGML_TYPE_Q4_K || op->src[0]->type == GGML_TYPE_Q5_K || op->src[0]->type == GGML_TYPE_Q4_1 || op->src[0]->type == GGML_TYPE_Q5_1)) { // These are all f16-arithmetic dequant rounding errors that intermittently exceed the // tight 1e-7 NMSE threshold depending on the random test data (see ggml-quants.cpp // make_int8_weights/make_int4_weights: dequant is done in f16, not f32, to keep the // Convert/Subtract/Multiply chain fusable into GatherMatmulCompressed/FullyConnectedCompressed // for the shared non-test code paths). return {false, "GET_ROWS/SET_ROWS with ne[0] == 256 and type " + std::string(ggml_type_name(op->src[0]->type)) + " rejected due to f16-arithmetic dequant rounding errors that intermittently exceed 1e-7 NMSE threshold"}; } break; } case GGML_OP_RESHAPE: { if (strncmp(op->name, "ffn_norm_exps", sizeof("ffn_norm_exps") - 1) == 0) { return {false, "RESHAPE for ffn_norm_exps is not supported"}; } break; } case GGML_OP_ADD: case GGML_OP_MUL: case GGML_OP_SUB: { if (op->src[1]->op == GGML_OP_PERMUTE) { return {false, "ADD/MUL/SUB with PERMUTE src1 is not supported"}; } for (int i = 0; i < 4; i++) { if (op->src[0]->ne[i] != op->src[1]->ne[i] && (op->src[0]->ne[i] != 1 && op->src[1]->ne[i] != 1)) { return {false, "ADD/MUL/SUB with incompatible broadcast shapes: src0->ne[" + std::to_string(i) + "]=" + std::to_string(op->src[0]->ne[i]) + ", src1->ne[" + std::to_string(i) + "]=" + std::to_string(op->src[1]->ne[i])}; } } break; } case GGML_OP_ADD_ID: { // Keep support aligned with the CPU backend implementation, which only handles f32 inputs/output and i32 ids. if (op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32 || op->src[1]->type != GGML_TYPE_F32 || op->src[2]->type != GGML_TYPE_I32) { return {false, "ADD_ID only supports F32 inputs/output and I32 ids"}; } break; } case GGML_OP_DIV: { // The GPU plugin can fuse broadcast DIV into the preceding FFN GEMM path // and produce infs for per-channel scale vectors. Keep those DIVs on CPU // until the fused GPU kernel is reliable. (falied case llama-arch-test mpt) if (ggml_openvino_get_device_name() == "GPU" && op->src[1]->ne[0] == op->ne[0] && op->src[1]->ne[1] == 1 && op->src[1]->ne[2] == 1 && op->src[1]->ne[3] == 1) { return {false, "DIV per-channel scale broadcast is not supported on GPU"}; } break; } case GGML_OP_POOL_2D: { const auto& name = ggml_openvino_get_device_name(); if (name == "GPU") { const int32_t * params = op->op_params; const int k0 = params[1]; const int k1 = params[2]; const int p0 = params[5]; const int p1 = params[6]; if ((p0 > 0 || p1 > 0) && (k0 < 3 || k1 < 3)) { return {false, "POOL_2D with padding and kernel size < 3 is not supported on " + name}; } } break; } case GGML_OP_SUM_ROWS: { if (op->src[0]->op == GGML_OP_PERMUTE) { return {false, "SUM_ROWS with PERMUTE input is not supported"}; } break; } case GGML_OP_FLASH_ATTN_EXT: { float scale = 1.0f; float max_bias = 0.0f; float logit_softcap = 0.0f; const auto * op_params = op->op_params; memcpy(&scale, (const float *) op_params + 0, sizeof(float)); memcpy(&max_bias, (const float *) op_params + 1, sizeof(float)); memcpy(&logit_softcap, (const float *) op_params + 2, sizeof(float)); // Keep gemma3n flash-attn pattern on CPU for GPU runs to avoid // accuracy drift in the OpenVINO path. Restrict by scale=1.0 to avoid // affecting non-gemma3n models such as Llama-3.2. if (fabsf(scale - 1.0f) < 1e-6f && is_gemma3n_flash_attn_pattern(op)) { return {false, "FLASH_ATTN_EXT gemma3n pattern on GPU is not supported"}; } if (op->src[4] != nullptr) { return {false, "FLASH_ATTN_EXT with sinks is not supported"}; } if (!is_supported_flash_attn_pattern(op)) { return {false, "FLASH_ATTN_EXT unsupported attention pattern"}; } if (max_bias > 0) { return {false, "FLASH_ATTN_EXT with max_bias > 0 (max_bias=" + std::to_string(max_bias) + ") is not supported"}; } if (logit_softcap != 0) { return {false, "FLASH_ATTN_EXT with logit_softcap != 0 (logit_softcap=" + std::to_string(logit_softcap) + ") is not supported"}; } break; } case GGML_OP_PERMUTE: { if (op->type == GGML_TYPE_BF16 && ggml_openvino_get_device_name() == "GPU") { return {false, "PERMUTE with BF16 type is not supported on GPU"}; } break; } case GGML_OP_CPY: { if (op->src[0]->type == GGML_TYPE_BF16 || op->src[1]->type == GGML_TYPE_BF16) { return {false, "CPY with BF16 src type is not supported"}; } // CPY to a quantized destination (e.g. f32 -> q4_0) is numerically unstable with OpenVINO backend. if (ggml_is_quantized(op->type)) { return {false, "CPY to quantized destination (e.g. f32 -> q4_0) is numerically unstable"}; } if (ggml_nelements(op->src[0]) != ggml_nelements(op->src[1])) { return {false, "CPY with mismatched element counts is not supported: src0=" + std::to_string(ggml_nelements(op->src[0])) + " != src1=" + std::to_string(ggml_nelements(op->src[1]))}; } // op test case with non-contiguous src or dst if ((op->ne[0] == 3 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) || (op->ne[0] == 1 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) || (op->ne[0] == 2 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2)) { return {false, "CPY with non-contiguous shape [" + std::to_string(op->ne[0]) + ", " + std::to_string(op->ne[1]) + ", " + std::to_string(op->ne[2]) + ", " + std::to_string(op->ne[3]) + "] is not supported"}; } if (!cpy_output_view_is_supported(op)) { return {false, "CPY with non-contiguous output view is not supported"}; } break; } case GGML_OP_MUL_MAT: { if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[1] != nullptr && ggml_is_quantized(op->src[0]->type) && strcmp(op->src[0]->name, "a") == 0 && strcmp(op->src[1]->name, "b") == 0 && op->src[0]->ne[1] == 1 && op->src[1]->ne[1] == 64 && op->src[0]->ne[0] == 256 && op->src[1]->ne[0] == 256) { return {false, "MUL_MAT quantized benchmark test case on GPU is not supported"}; } if (op->src[0]->ne[3] != op->src[1]->ne[3] && op->src[0]->ne[3] != 1 && op->src[1]->ne[3] != 1) { return {false, "MUL_MAT with incompatible broadcast on ne[3]: src0->ne[3]=" + std::to_string(op->src[0]->ne[3]) + ", src1->ne[3]=" + std::to_string(op->src[1]->ne[3])}; } if (op->src[0]->op == GGML_OP_VIEW && op->src[1]->op == GGML_OP_VIEW) { return {false, "MUL_MAT with both inputs as VIEW is not supported"}; } break; } case GGML_OP_MUL_MAT_ID: { // Single-expert (or empty) MUL_MAT_ID is a degenerate shape that stresses GatherMatmul edge // cases and never occurs in real MoE; let it fall back to CPU. if (op->src[0] != nullptr && op->src[0]->ne[2] <= 1) { return {false, "MUL_MAT_ID with single-expert or empty ne[2] <= 1 (ne[2]=" + std::to_string(op->src[0]->ne[2]) + ") is not supported"}; } if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) { return {false, "MUL_MAT_ID with BF16 weights on GPU is not supported"}; } // GPU MUL_MAT_ID uses a Gather+MatMul fallback because the GPU plugin rejects internal // GatherMatmul for these test shapes. Skip cases that would materialize a large selected // expert-weight temporary. if (ggml_openvino_get_device_name() == "GPU" && mul_mat_id_requires_large_tmp(op)) { return {false, "MUL_MAT_ID requires large temporary on GPU"}; } break; } case GGML_OP_ROPE: { const int32_t * op_params = op->op_params; const int n_dims = op_params[1]; const int mode = op_params[2]; if (op_params[15] != 0) { // FIXME: support ggml_rope_set_offset return {false, "ggml_rope_set_offset is not supported"}; } if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX && mode != GGML_ROPE_TYPE_IMROPE) { return {false, "ROPE with mode " + std::to_string(mode) + " is not supported"}; } const int64_t head_dim = op->src[0]->ne[0]; const int64_t rope_dims = n_dims == 0 ? head_dim : n_dims; if (rope_dims <= 0 || rope_dims > head_dim || (rope_dims % 2) != 0) { return {false, "ROPE with n_dims=" + std::to_string(n_dims) + ", head_dim=" + std::to_string(head_dim) + " is not supported"}; } if (op->type != GGML_TYPE_F32 && op->type != GGML_TYPE_F16) { return {false, "ROPE with type " + std::string(ggml_type_name(op->type)) + " is not supported"}; } if (op->src[0]->op == GGML_OP_VIEW) { const struct ggml_tensor * view = op->src[0]; const struct ggml_tensor * view_src = view->view_src; if (view_src->ne[1] != view->ne[1] || view_src->ne[2] != view->ne[2] || view_src->ne[3] != view->ne[3]) { return {false, "ROPE with view_src->ne [" + std::to_string(view_src->ne[1]) + ", " + std::to_string(view_src->ne[2]) + ", " + std::to_string(view_src->ne[3]) + "] != view->ne [" + std::to_string(view->ne[1]) + ", " + std::to_string(view->ne[2]) + ", " + std::to_string(view->ne[3]) + "] is not supported"}; } } if (mode == GGML_ROPE_TYPE_IMROPE && (op->src[2] != 0 || ((const float *) op_params)[6] != 1 || ((const float *) op_params)[7] != 0 || ((const float *) op_params)[8] != 1)) { return {false, "IMROPE with freq_factors, freq_scale, ext_factor, and attn_factor is not supported"}; } break; } case GGML_OP_TRANSPOSE: { if (op->type == GGML_TYPE_BF16) { return {false, "TRANSPOSE with BF16 type is not supported"}; } break; } case GGML_OP_REPEAT: { if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16) { return {false, "REPEAT with BF16 type is not supported on GPU"}; } break; } case GGML_OP_GATED_DELTA_NET: { // enable after https://github.com/openvinotoolkit/openvino/pull/35917 is included in OV release // return true; // if (ggml_openvino_get_device_name() == "GPU" && op->src[0]->ne[2] > 1) { // // CVS-186471 // return true; // } if (op->src[2]->op == GGML_OP_PERMUTE) { return {false, "GATED_DELTA_NET with PERMUTE src2 is not supported"}; } // kda (per-key-dimension gating) not supported by fused GatedDeltaNet op if (op->src[3]->ne[0] != 1) { return {false, "GATED_DELTA_NET with kda (per-key-dimension gating) is not supported"}; } // K > 1 (multiple state snapshots) not supported by fused op if (((const int32_t *) op->op_params)[0] > 1) { return {false, "GATED_DELTA_NET with K > 1 (multiple state snapshots) is not supported"}; } break; } case GGML_OP_SSM_CONV: { // qwen3next is numerically unstable with OpenVINO SSM_CONV. // Keep this op on CPU until the OpenVINO implementation is fixed. // return true; break; } case GGML_OP_VIEW: { // Skip TOPK_MOE fused tests until it is fully supported. // The argsort_top_k VIEW wrapping ARGSORT is named "selected_experts" in test_topk_moe. if (strcmp(op->name, "selected_experts") == 0) { return {false, "VIEW for selected_experts (argsort_top_k) is not supported"}; } break; } default: break; } return {true, ""}; } static ggml_openvino_op_support ggml_backend_openvino_device_supports_op_impl(ggml_backend_dev_t dev, const ggml_tensor * op) { GGML_ASSERT(dev->reg != nullptr); static std::unordered_set supported_types{ GGML_TYPE_F32, GGML_TYPE_F16, GGML_TYPE_BF16, GGML_TYPE_I64, GGML_TYPE_I32, GGML_TYPE_Q4_0, GGML_TYPE_Q4_1, GGML_TYPE_Q4_K, GGML_TYPE_Q5_1, GGML_TYPE_Q5_K, GGML_TYPE_Q8_0, GGML_TYPE_Q6_K, GGML_TYPE_MXFP4}; // derive supported op sets from the op_table map, keys in // the map use the full macro name (e.g. "GGML_OP_ADD"), while // the ggml_*_op_name() helpers return only the trailing part (e.g. "ADD"). // each set is built once and cached. static const auto build_supported_sets = [] { const auto & table = ov::frontend::ggml::get_supported_ops(); std::unordered_set ops; std::unordered_set unary_ops; std::unordered_set glu_ops; // GGML_OP_NONE has no translator but is always safe to add to the supported set. ops.insert(GGML_OP_NONE); for (int i = 0; i < GGML_OP_COUNT; ++i) { const std::string key = std::string("GGML_OP_") + ggml_op_name(static_cast(i)); if (table.count(key)) { ops.insert(static_cast(i)); } } for (int i = 0; i < GGML_UNARY_OP_COUNT; ++i) { const std::string key = std::string("GGML_UNARY_OP_") + ggml_unary_op_name(static_cast(i)); if (table.count(key)) { unary_ops.insert(static_cast(i)); } } for (int i = 0; i < GGML_GLU_OP_COUNT; ++i) { const std::string key = std::string("GGML_GLU_OP_") + ggml_glu_op_name(static_cast(i)); if (table.count(key)) { glu_ops.insert(static_cast(i)); } } return std::make_tuple(ops, unary_ops, glu_ops); }; static const auto supported_sets = build_supported_sets(); static const auto & supported_ops = std::get<0>(supported_sets); static const auto & supported_unary_ops = std::get<1>(supported_sets); static const auto & supported_glu_ops = std::get<2>(supported_sets); switch (op->op) { case GGML_OP_UNARY: { auto supported = supported_unary_ops.find(ggml_get_unary_op(op)) != supported_unary_ops.end(); if (!supported) { return {false, "unary op " + std::string(ggml_unary_op_name(ggml_get_unary_op(op))) + " has no op translator"}; } if (ggml_get_unary_op(op) == GGML_UNARY_OP_EXP && op->type == GGML_TYPE_F32) { return {false, "UNARY_EXP with F32 type is not supported"}; } break; } case GGML_OP_GLU: { auto supported = supported_glu_ops.find(ggml_get_glu_op(op)) != supported_glu_ops.end(); if (!supported) { return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " has no op translator"}; } // if (has_view_op_input(op)) { // return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " with view input is not supported"}; // } if (op->src[1] == nullptr && op->src[0]->ne[0] % 2 != 0) { // triggers bug in ov gpu return {false, "GLU op with odd src0 ne[0] and null src1 is not supported"}; } break; } default: { auto supported = supported_ops.find(op->op) != supported_ops.end(); if (!supported) { return {false, "op " + std::string(ggml_op_name(op->op)) + " has no op translator"}; } static std::set ops_not_support_view_input{}; if (ops_not_support_view_input.find(op->op) != ops_not_support_view_input.end() && has_view_op_input(op)) { return {false, "op " + std::string(ggml_op_name(op->op)) + " with VIEW input is not supported"}; } } } if (supported_types.find(op->type) == supported_types.end()) { return {false, "tensor type " + std::string(ggml_type_name(op->type)) + " is not supported"}; } for (int i = 0; i < GGML_MAX_SRC; i++) { auto * src = op->src[i]; if (src == nullptr) { break; } if (supported_types.find(src->type) == supported_types.end()) { return {false, "src[" + std::to_string(i) + "] type " + std::string(ggml_type_name(src->type)) + " is not supported"}; } const bool is_supported_3d_moe_expert = op->op == GGML_OP_MUL_MAT_ID && i == 0 && (src->type == GGML_TYPE_MXFP4 || src->ne[3] == 1); if (ggml_is_quantized(src->type) && src->ne[2] != 1 && !is_supported_3d_moe_expert) { return {false, "3D quantized tensor for src[" + std::to_string(i) + "] is not supported"}; } } auto op_support_case = is_op_supported_case(op); if (!op_support_case.is_supported) { return op_support_case; } return {true, ""}; } static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, const ggml_tensor * op) { auto res = ggml_backend_openvino_device_supports_op_impl(dev, op); if (!res.is_supported) { static const bool log_unsupported = ggml_openvino_getenv_int("GGML_OPENVINO_LOG_UNSUPPORTED_OPS") != 0; if (log_unsupported) { GGML_LOG_WARN("OpenVINO op unsupported: op '%s' (%s), type %s: %s\n", op->name, ggml_op_name(op->op), ggml_type_name(op->type), res.reason.c_str()); } } return res.is_supported; } static bool ggml_backend_openvino_device_supports_buft(ggml_backend_dev_t dev, ggml_backend_buffer_type_t buft) { return ggml_backend_buft_is_openvino(buft) || ggml_backend_buft_is_host(buft); GGML_UNUSED(dev); } static const struct ggml_backend_device_i ggml_backend_openvino_device_interface = { /* .get_name = */ ggml_backend_openvino_device_get_name, /* .get_description = */ ggml_backend_openvino_device_get_description, /* .get_memory = */ ggml_backend_openvino_device_get_memory, /* .get_type = */ ggml_backend_openvino_device_get_type, /* .get_props = */ ggml_backend_openvino_device_get_props, /* .init_backend = */ ggml_backend_openvino_device_init, /* .get_buffer_type = */ ggml_backend_openvino_device_get_buffer_type, /* .get_host_buffer_type = */ ggml_backend_openvino_device_get_host_buffer_type, /* .buffer_from_host_ptr = */ NULL, /* .supports_op = */ ggml_backend_openvino_device_supports_op, /* .supports_buft = */ ggml_backend_openvino_device_supports_buft, /* .offload_op = */ NULL, /* .event_new = */ NULL, /* .event_free = */ NULL, /* .event_synchronize = */ NULL, }; struct ggml_backend_openvino_reg_context { std::vector devices; }; static const char * ggml_backend_openvino_reg_get_name(ggml_backend_reg_t reg) { return GGML_OPENVINO_NAME; GGML_UNUSED(reg); } static size_t ggml_backend_openvino_reg_get_device_count(ggml_backend_reg_t reg) { GGML_UNUSED(reg); return (size_t) ggml_backend_openvino_get_device_count(); } static ggml_backend_dev_t ggml_backend_openvino_reg_get_device(ggml_backend_reg_t reg, size_t index) { ggml_backend_openvino_reg_context * ctx = (ggml_backend_openvino_reg_context *) reg->context; GGML_ASSERT(index < ctx->devices.size()); return ctx->devices[index]; } static const struct ggml_backend_reg_i ggml_backend_openvino_reg_interface = { /* .get_name = */ ggml_backend_openvino_reg_get_name, /* .get_device_count = */ ggml_backend_openvino_reg_get_device_count, /* .get_device = */ ggml_backend_openvino_reg_get_device, /* .get_proc_address = */ NULL, }; static void ggml_openvino_init() { // Initialize device config singleton from env var ggml_openvino_init_device_config(); GGML_LOG_INFO("OpenVINO: using device %s\n", ggml_openvino_get_device_name().c_str()); } GGML_BACKEND_API ggml_backend_reg_t ggml_backend_openvino_reg(void) { static ggml_backend_reg reg; static bool initialized = false; { static std::mutex mutex; std::lock_guard lock(mutex); if (!initialized) { ggml_openvino_init(); ggml_backend_openvino_reg_context * ctx = new ggml_backend_openvino_reg_context; for (int i = 0; i < ggml_backend_openvino_get_device_count(); i++) { ggml_backend_openvino_device_context * dev_ctx = new ggml_backend_openvino_device_context; dev_ctx->device = i; dev_ctx->name = GGML_OPENVINO_NAME + std::to_string(i); dev_ctx->description = ov::get_openvino_version().description; ggml_backend_dev_t dev = new ggml_backend_device{/* .interface = */ ggml_backend_openvino_device_interface, /* .reg = */ ®, /* .context = */ dev_ctx}; ctx->devices.push_back(dev); } reg = ggml_backend_reg{/* .api_version = */ GGML_BACKEND_API_VERSION, /* .iface = */ ggml_backend_openvino_reg_interface, /* .context = */ ctx}; } initialized = true; } return ® } GGML_BACKEND_DL_IMPL(ggml_backend_openvino_reg)