char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Jeff Bolzvulkan: support type-aligned GET_ROWS (#28253)0cae43063

char/aliceai-t5-moe
66.5 KiB1570 linesraw
1#include "ggml-openvino.h"
2
3#include "ggml-backend-impl.h"
4#include "ggml-backend.h"
5#include "ggml-impl.h"
6#include "ggml-openvino-extra.h"
7#include "ggml-openvino/openvino/op_table.h"
8#include "ggml-openvino/utils.h"
9#include "ggml-quants.h"
10#include "ggml.h"
11
12#include <atomic>
13#include <cstdint>
14#include <cstdlib>
15#include <cstring>
16#include <memory>
17#include <mutex>
18#include <openvino/core/type/element_type.hpp>
19#include <openvino/openvino.hpp>
20#include <openvino/runtime/allocator.hpp>
21#include <openvino/runtime/intel_gpu/ocl/ocl.hpp>
22#include <openvino/runtime/intel_npu/level_zero/level_zero.hpp>
23#include <openvino/runtime/tensor.hpp>
24#include <set>
25#include <string>
26#include <vector>
27
28#if defined(_WIN32)
29#    define WIN32_LEAN_AND_MEAN
30#    ifndef NOMINMAX
31#        define NOMINMAX
32#    endif
33#    include <windows.h>
34#else
35#    include <sys/mman.h>
36#    include <unistd.h>
37#endif
38
39// =====================================================
40// OpenVINO Buffer Implementation using ov::Tensor
41// =====================================================
42//
43// Design: This implementation uses a hybrid approach:
44// 1. For weight tensors: Store a pre-built ov::op::v0::Constant in tensor->extra
45//    - This avoids the memcpy during graph construction
46//    - For quantized weights, the constant is already converted to OpenVINO format
47// 2. For KV cache / compute tensors: Store an ov::Tensor in tensor->extra
48//    - This can be directly passed to infer_request
49//    - Future: can be changed to ov::RemoteTensor for GPU/NPU
50//
51// This design is similar to:
52// - CUDA split buffer: tensor->extra stores device pointers
53// - CPU repack buffer: tensor->extra stores tensor_traits with repacked data
54// =====================================================
55
56// Buffer context that manages per-tensor allocations (no contiguous buffer for weights)
57struct ggml_backend_openvino_buffer_context {
58    int device;
59    std::string name;
60    size_t id;
61
62    // For non-weight buffers (KV cache, compute), we still use contiguous allocation
63    void * data;
64    size_t size;
65    bool is_remote;
66
67    // Wrapping of the buffer
68    std::shared_ptr<ov::Tensor> ov_buffer;
69
70    // Track all extras for cleanup
71    std::map<ggml_tensor *, ggml_openvino_extra_base *> tensor_extras;
72
73    // Used for re-allocation on device for kvcache
74    void * data_prev;
75
76    ggml_backend_openvino_buffer_context(int device, size_t size, bool is_remote = false) :
77        device(device),
78        name(std::string(GGML_OPENVINO_NAME) + std::to_string(device)),
79        id([]() {
80            static std::atomic<size_t> next_id{1};
81            return next_id.fetch_add(1);
82        }()),
83        data(nullptr),
84        size(size),
85        is_remote(is_remote) {
86        if (size == 0) {
87            return;
88        }
89
90        const auto & device_name = ggml_openvino_get_device_name();
91
92        if (is_remote) {
93            GGML_ASSERT(device_name == "GPU");
94            auto remote_context = ggml_openvino_get_remote_context();
95            auto gpu_context = remote_context->as<ov::intel_gpu::ocl::ClContext>();
96            ov::intel_gpu::ocl::USMTensor usm_tensor =
97                gpu_context.create_usm_device_tensor(ov::element::u8, ov::Shape{size});
98            data = usm_tensor.get();
99            ov_buffer = std::make_shared<ov::intel_gpu::ocl::USMTensor>(std::move(usm_tensor));
100        } else {
101            data = ggml_aligned_malloc(size);
102            GGML_ASSERT(data);
103            memset(data, 0, size);
104            ov_buffer = std::make_shared<ov::Tensor>(ov::element::u8, ov::Shape{size}, data);
105        }
106
107        if (data == nullptr) {
108            GGML_LOG_ERROR("%s: failed to allocate %zu bytes\n", __func__, size);
109            return;
110        }
111
112        if (reinterpret_cast<uintptr_t>(data) % TENSOR_ALIGNMENT != 0) {
113            GGML_LOG_ERROR("%s: %s buffer is not aligned to %d bytes\n", __func__, device_name.c_str(),
114                           TENSOR_ALIGNMENT);
115            GGML_ABORT("fatal error");
116        }
117    }
118
119    ~ggml_backend_openvino_buffer_context() {
120        // Clean up all tensor extras
121        // GGML_LOG_DEBUG("Deleting OpenVINO buffer context #%zu for device %d, size %zu MB\n", id, device,
122        //                size / 1024 / 1024);
123        for (auto & pair : tensor_extras) {
124            delete pair.second;
125        }
126        tensor_extras.clear();
127        if (!is_remote && data != nullptr) {
128            ggml_aligned_free(data, size);
129        }
130    }
131};
132
133// Buffer type context (per-device)
134struct ggml_backend_openvino_buffer_type_context {
135    int device;
136    std::string name;
137};
138
139// =====================================================
140// Host weight-buffer release (GGML_OPENVINO_RELEASE_WEIGHTS)
141// =====================================================
142// The OpenVINO weight Constants are zero-copy views into the host buffers
143// allocated here (ggml_aligned_malloc, anonymous memory). On GPU the plugin
144// holds its own device copy after compile_model, so the host pages are dead
145// weight for inference and can be dropped to reclaim RSS (~weights size).
146//
147// We do NOT free the buffer (ggml owns its lifetime and tensors still point
148// into it); instead madvise(MADV_DONTNEED) drops the resident pages while
149// keeping the mapping valid. A later recompile would re-read these Constants
150// from now-zeroed memory and produce garbage, so once released we fail fast
151// if the cache-miss compile branch is reached again (see utils.cpp).
152namespace {
153struct ov_weight_buffer_registry {
154    std::mutex mutex;
155    // (data, size) of every non-remote weight buffer, for madvise.
156    std::vector<std::pair<void *, size_t>> buffers;
157    bool released = false;
158};
159
160ov_weight_buffer_registry & ov_weight_registry() {
161    static ov_weight_buffer_registry reg;
162    return reg;
163}
164}  // namespace
165
166void ggml_openvino_register_weight_buffer(void * data, size_t size) {
167    if (data == nullptr || size == 0) {
168        return;
169    }
170    auto & reg = ov_weight_registry();
171    std::lock_guard<std::mutex> lock(reg.mutex);
172    for (const auto & b : reg.buffers) {
173        if (b.first == data) {
174            return;  // already registered
175        }
176    }
177    reg.buffers.emplace_back(data, size);
178}
179
180bool ggml_openvino_weight_buffers_released() {
181    auto & reg = ov_weight_registry();
182    std::lock_guard<std::mutex> lock(reg.mutex);
183    return reg.released;
184}
185
186void ggml_openvino_release_weight_buffers() {
187    auto & reg = ov_weight_registry();
188    std::lock_guard<std::mutex> lock(reg.mutex);
189    if (reg.released) {
190        return;
191    }
192    size_t total = 0;
193#if !defined(_WIN32)
194    for (const auto & b : reg.buffers) {
195        // Align down/up to page boundaries so madvise only drops whole pages
196        // fully owned by this buffer.
197        const long page = sysconf(_SC_PAGESIZE);
198        uintptr_t start = reinterpret_cast<uintptr_t>(b.first);
199        uintptr_t end = start + b.second;
200        uintptr_t astart = (start + page - 1) & ~(uintptr_t) (page - 1);
201        uintptr_t aend = end & ~(uintptr_t) (page - 1);
202        if (aend > astart) {
203            if (madvise(reinterpret_cast<void *>(astart), aend - astart, MADV_DONTNEED) == 0) {
204                total += aend - astart;
205            }
206        }
207    }
208#endif
209    reg.released = true;
210    GGML_LOG_INFO("%s: released %zu MB of host weight buffers (%zu buffers)\n", __func__, total / 1024 / 1024,
211                  reg.buffers.size());
212}
213
214// Buffer interface functions
215static void ggml_backend_openvino_buffer_free_buffer(ggml_backend_buffer_t buffer) {
216    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
217    delete ctx;
218}
219
220static void * ggml_backend_openvino_buffer_get_base(ggml_backend_buffer_t buffer) {
221    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
222    return ctx->data;
223}
224
225static bool is_stateful_enabled() {
226    return ggml_openvino_getenv_int("GGML_OPENVINO_STATEFUL_EXECUTION") != 0;
227}
228
229static enum ggml_status ggml_backend_openvino_buffer_init_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor) {
230    // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name);
231    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
232
233    // Put kvcache on device memory for GPU (NPU memory is too small even for kvcache)
234    if (strncmp(tensor->name, "cache_", 6) == 0 && !ctx->is_remote && ggml_openvino_get_device_name() == "GPU" &&
235        !is_stateful_enabled()) {
236        GGML_ASSERT(ctx->tensor_extras.empty());
237        auto device = ctx->device;
238        auto size = ctx->size;
239        auto * data_prev = ctx->data;
240        delete ctx;
241        ctx = new ggml_backend_openvino_buffer_context(device, size, true);
242        buffer->context = ctx;
243        tensor->data = (char *) ctx->data + ((char *) tensor->data - (char *) data_prev);
244    }
245
246    // Views share the extra from view_src
247    if (tensor->view_src != nullptr) {
248        GGML_ASSERT(tensor->view_src->buffer->buft == buffer->buft);
249        if (tensor->view_src->extra != nullptr) {
250            tensor->extra = tensor->view_src->extra;
251        }
252        return GGML_STATUS_SUCCESS;
253    }
254
255    ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
256
257    if (tensor->data != nullptr && !ggml_is_quantized(tensor->type)) {
258        ggml_openvino_tensor_extra * extra = ggml_openvino_create_tensor_extra(tensor, ctx->is_remote);
259        if (extra != nullptr) {
260            auto it = ctx->tensor_extras.find(tensor);
261            if (it != ctx->tensor_extras.end()) {
262                delete it->second;
263            }
264            ctx->tensor_extras[tensor] = extra;
265            tensor->extra = extra;
266        }
267    }
268
269    return GGML_STATUS_SUCCESS;
270}
271
272static void ggml_backend_openvino_buffer_memset_tensor(ggml_backend_buffer_t buffer,
273                                                       ggml_tensor * tensor,
274                                                       uint8_t value,
275                                                       size_t offset,
276                                                       size_t size) {
277    // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name);
278    GGML_ASSERT(tensor != nullptr && tensor->data != nullptr);
279    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
280
281    if (ctx->is_remote) {
282        // For remote (device) buffers, use OpenCL USM memfill
283        cl_command_queue queue = ggml_openvino_get_cl_queue();
284        auto mem_fill_fn = ggml_openvino_get_clEnqueueMemFillINTEL();
285        if (queue != nullptr && mem_fill_fn != nullptr) {
286            uint8_t pattern = value;
287            cl_int err = mem_fill_fn(queue, (char *) tensor->data + offset, &pattern, sizeof(pattern), size, 0, nullptr,
288                                     nullptr);
289            if (err != CL_SUCCESS) {
290                GGML_LOG_ERROR("%s: clEnqueueMemFillINTEL failed with error %d\n", __func__, err);
291            }
292            clFinish(queue);
293        } else {
294            GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemFillINTEL not available for GPU buffer\n", __func__);
295        }
296    } else {
297        memset((char *) tensor->data + offset, value, size);
298    }
299}
300
301static void ggml_backend_openvino_buffer_set_tensor(ggml_backend_buffer_t buffer,
302                                                    ggml_tensor * tensor,
303                                                    const void * data,
304                                                    size_t offset,
305                                                    size_t size) {
306    // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name);
307    GGML_ASSERT(tensor != nullptr && tensor->data != nullptr);
308    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
309
310    // Check if this is a weight buffer (usage is set BEFORE set_tensor is called, except in test-backend-ops)
311    bool is_weight_buffer = (buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS);
312    // Full tensor set: offset=0, full size, not a view
313    bool is_full_tensor_set = (offset == 0 && size == ggml_nbytes(tensor) && tensor->view_src == nullptr);
314    // 2D tensor (typical weight shape), or a 3D quantized MoE expert weight (MUL_MAT_ID). Dense 3D
315    // expert weights are handled later in create_weight_node instead.
316    bool is_2d = (tensor->ne[2] == 1 && tensor->ne[3] == 1);
317    bool is_supported_weight_shape = is_2d || (tensor->ne[3] == 1 && ggml_is_quantized(tensor->type));
318
319    if (is_weight_buffer && is_full_tensor_set && is_supported_weight_shape) {
320        try {
321            auto result = process_weight_tensor(tensor, data, tensor->data);
322            result.weight_node->set_friendly_name(tensor->name);
323
324            // const auto & layout = result.layout;
325            ggml_openvino_extra_base * extra;
326
327            // Quantized path with extracted weight/scale/zp tensors
328            if (result.is_quantized()) {
329                extra = new ggml_openvino_quantized_weight_extra(std::move(result.weights), std::move(result.scales),
330                                                                 std::move(result.zp), result.weight_node);
331
332                // if (layout.is_requant) {
333                //     GGML_LOG_DEBUG("%s: requantized %s to %s (u%d, block_size=%ld)\n", __func__, tensor->name,
334                //                    extra_quant_type_name(layout.requant_type.value()), layout.is_u4 ? 4 : 8,
335                //                    layout.weights_per_block);
336                // } else {
337                //     int64_t n_blocks = ggml_nelements(tensor) / layout.weights_per_block;
338                //     GGML_LOG_DEBUG("%s: extracted quantized weight node for %s (u%d, %zu weights, %ld blocks)\n",
339                //                    __func__, tensor->name, layout.is_u4 ? 4 : 8, layout.weights_size, n_blocks);
340                // }
341            } else {
342                // F16/F32/BF16 weight or F16-requant
343                extra = new ggml_openvino_weight_extra(std::move(result.weights), result.weight_node);
344
345                // if (layout.total_size > 0) {
346                //     GGML_LOG_DEBUG("%s: requantized %s to F16\n", __func__, tensor->name);
347                // } else {
348                //     GGML_LOG_DEBUG("%s: created shared-memory weight node for %s\n", __func__, tensor->name);
349                // }
350            }
351
352            ctx->tensor_extras[tensor] = extra;
353            tensor->extra = extra;
354
355            // Register the host buffer so its pages can be dropped after the GPU
356            // plugin has its own device copy (GGML_OPENVINO_RELEASE_WEIGHTS).
357            if (!ctx->is_remote) {
358                // Weights are set once at model load. Setting a weight after a release
359                // means a second model is loading while the first's compiled graph is
360                // pinned — that graph would be wrongly reused with this model's key.
361                // Fail loud rather than return silently-wrong results.
362                if (ggml_openvino_weight_buffers_released()) {
363                    GGML_ABORT(
364                        "ggml-openvino: loading a new model while GGML_OPENVINO_RELEASE_WEIGHTS pinned a previous "
365                        "model's compiled graph. This mode supports a single model per process; unset it for "
366                        "multi-model runs.");
367                }
368                ggml_openvino_register_weight_buffer(ctx->data, ctx->size);
369            }
370
371        } catch (const std::exception & e) {
372            GGML_LOG_ERROR("%s: failed to process weight tensor for %s: %s\n", __func__, tensor->name, e.what());
373            memcpy((char *) tensor->data + offset, data, size);
374        }
375    } else {
376        // Non-weight tensor (KV cache, activations, etc.) - copy data. test-backend-ops also goes here
377        if (ctx->is_remote) {
378            cl_command_queue queue = ggml_openvino_get_cl_queue();
379            auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL();
380            if (queue != nullptr && mem_cpy_fn != nullptr) {
381                cl_int err =
382                    mem_cpy_fn(queue, CL_TRUE, (char *) tensor->data + offset, data, size, 0, nullptr, nullptr);
383                if (err != CL_SUCCESS) {
384                    GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL failed with error %d\n", __func__, err);
385                }
386            } else {
387                GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__);
388            }
389        } else {
390            memcpy((char *) tensor->data + offset, data, size);
391        }
392
393        ggml_openvino_tensor_extra * extra = ggml_openvino_create_tensor_extra(tensor, ctx->is_remote);
394        if (extra == nullptr) {
395            // GGML_LOG_ERROR("%s: failed to create tensor extra for %s\n", __func__, tensor->name);
396            return;
397        }
398
399        auto it = ctx->tensor_extras.find(tensor);
400        if (it != ctx->tensor_extras.end()) {
401            delete it->second;
402        }
403        ctx->tensor_extras[tensor] = extra;
404        tensor->extra = extra;
405    }
406}
407
408static void ggml_backend_openvino_buffer_get_tensor(ggml_backend_buffer_t buffer,
409                                                    const ggml_tensor * tensor,
410                                                    void * data,
411                                                    size_t offset,
412                                                    size_t size) {
413    // GGML_LOG_DEBUG("%s: buffer usage=%d, tensor name=%s\n", __func__, buffer->usage, tensor->name);
414    GGML_ASSERT(tensor != nullptr && tensor->data != nullptr);
415    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
416
417    if (ctx->is_remote) {
418        // For remote (device) buffers, use OpenCL USM memcpy (device-to-host)
419        cl_command_queue queue = ggml_openvino_get_cl_queue();
420        auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL();
421        if (queue != nullptr && mem_cpy_fn != nullptr) {
422            cl_int err =
423                mem_cpy_fn(queue, CL_TRUE, data, (const char *) tensor->data + offset, size, 0, nullptr, nullptr);
424            if (err != CL_SUCCESS) {
425                GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL failed with error %d\n", __func__, err);
426            }
427        } else {
428            GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__);
429        }
430    } else {
431        memcpy(data, (const char *) tensor->data + offset, size);
432    }
433}
434
435static bool ggml_backend_openvino_buffer_cpy_tensor(ggml_backend_buffer_t buffer,
436                                                    const ggml_tensor * src,
437                                                    ggml_tensor * dst) {
438    // GGML_LOG_DEBUG("%s: src tensor name=%s, dst tensor name=%s\n", __func__, src->name, dst->name);
439    GGML_ASSERT(src != nullptr && dst != nullptr);
440    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
441
442    if (ctx->is_remote) {
443        // For remote (device) buffers, use OpenCL USM memcpy
444        cl_command_queue queue = ggml_openvino_get_cl_queue();
445        auto mem_cpy_fn = ggml_openvino_get_clEnqueueMemcpyINTEL();
446        if (queue == nullptr || mem_cpy_fn == nullptr) {
447            GGML_LOG_ERROR("%s: no OpenCL queue or clEnqueueMemcpyINTEL not available for GPU buffer\n", __func__);
448            return false;
449        }
450        // Can copy from host to device
451        if (ggml_backend_buffer_is_host(src->buffer)) {
452            cl_int err = mem_cpy_fn(queue, CL_TRUE, dst->data, src->data, ggml_nbytes(src), 0, nullptr, nullptr);
453            if (err != CL_SUCCESS) {
454                GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL (host-to-device) failed with error %d\n", __func__, err);
455                return false;
456            }
457            return true;
458        }
459        // Can also copy from device to device if both are OpenVINO remote buffers
460        if (ggml_backend_buffer_is_openvino(src->buffer)) {
461            ggml_backend_openvino_buffer_context * src_ctx =
462                (ggml_backend_openvino_buffer_context *) src->buffer->context;
463            if (src_ctx->is_remote) {
464                cl_int err = mem_cpy_fn(queue, CL_TRUE, dst->data, src->data, ggml_nbytes(src), 0, nullptr, nullptr);
465                if (err != CL_SUCCESS) {
466                    GGML_LOG_ERROR("%s: clEnqueueMemcpyINTEL (device-to-device) failed with error %d\n", __func__, err);
467                    return false;
468                }
469                return true;
470            }
471        }
472        return false;
473    }
474
475    // Host buffer - can copy from any host buffer
476    if (ggml_backend_buffer_is_host(src->buffer)) {
477        memcpy(dst->data, src->data, ggml_nbytes(src));
478        return true;
479    }
480    return false;
481}
482
483static void ggml_backend_openvino_buffer_clear(ggml_backend_buffer_t buffer, uint8_t value) {
484    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
485    GGML_ASSERT(ctx->data != nullptr);
486    if (ctx->is_remote) {
487        cl_command_queue queue = ggml_openvino_get_cl_queue();
488        auto mem_fill_fn = ggml_openvino_get_clEnqueueMemFillINTEL();
489        if (queue != nullptr && mem_fill_fn != nullptr) {
490            uint8_t pattern = value;
491            cl_int err = mem_fill_fn(queue, ctx->data, &pattern, sizeof(pattern), ctx->size, 0, nullptr, nullptr);
492            if (err != CL_SUCCESS) {
493                GGML_LOG_WARN("%s: clEnqueueMemFillINTEL failed with error %d\n", __func__, err);
494            }
495            clFinish(queue);
496        } else {
497            GGML_LOG_WARN("%s: no OpenCL queue or clEnqueueMemFillINTEL not available for GPU buffer clear\n",
498                          __func__);
499        }
500    } else {
501        memset(ctx->data, value, ctx->size);
502    }
503}
504
505static const ggml_backend_buffer_i ggml_backend_openvino_buffer_interface = {
506    /* .free_buffer     = */ ggml_backend_openvino_buffer_free_buffer,
507    /* .get_base        = */ ggml_backend_openvino_buffer_get_base,
508    /* .init_tensor     = */ ggml_backend_openvino_buffer_init_tensor,
509    /* .memset_tensor   = */ ggml_backend_openvino_buffer_memset_tensor,
510    /* .set_tensor      = */ ggml_backend_openvino_buffer_set_tensor,
511    /* .get_tensor      = */ ggml_backend_openvino_buffer_get_tensor,
512    /* .set_tensor_2d   = */ NULL,
513    /* .get_tensor_2d   = */ NULL,
514    /* .cpy_tensor      = */ ggml_backend_openvino_buffer_cpy_tensor,
515    /* .clear           = */ ggml_backend_openvino_buffer_clear,
516    /* .reset           = */ NULL,
517};
518
519// Buffer type interface functions
520static const char * ggml_backend_openvino_buffer_type_get_name(ggml_backend_buffer_type_t buft) {
521    ggml_backend_openvino_buffer_type_context * ctx = (ggml_backend_openvino_buffer_type_context *) buft->context;
522    return ctx->name.c_str();
523}
524
525static ggml_backend_buffer_t ggml_backend_openvino_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft,
526                                                                            size_t size) {
527    ggml_backend_openvino_buffer_type_context * buft_ctx = (ggml_backend_openvino_buffer_type_context *) buft->context;
528
529    // Create buffer context with contiguous memory allocation
530    ggml_backend_openvino_buffer_context * ctx = new ggml_backend_openvino_buffer_context(buft_ctx->device, size);
531
532    if (ctx->data == nullptr && size > 0) {
533        GGML_LOG_ERROR("%s: failed to allocate buffer of size %zu\n", __func__, size);
534        delete ctx;
535        return nullptr;
536    }
537
538    return ggml_backend_buffer_init(buft, ggml_backend_openvino_buffer_interface, ctx, size);
539}
540
541static size_t ggml_backend_openvino_buffer_type_get_alignment(ggml_backend_buffer_type_t buft) {
542    GGML_UNUSED(buft);
543    return TENSOR_ALIGNMENT;
544}
545
546static size_t ggml_backend_openvino_buffer_type_get_max_size(ggml_backend_buffer_type_t buft) {
547    GGML_UNUSED(buft);
548    return SIZE_MAX;
549}
550
551static size_t ggml_backend_openvino_buffer_type_get_alloc_size(ggml_backend_buffer_type_t buft,
552                                                               const ggml_tensor * tensor) {
553    GGML_UNUSED(buft);
554
555    // For quantized weight tensors, we need extra space for extracted data.
556    if (ggml_is_quantized(tensor->type) && tensor->ne[3] == 1) {
557        ggml_openvino_extracted_layout layout = ggml_openvino_get_extracted_layout(tensor);
558        if (layout.total_size > 0) {
559            // GGML_LOG_DEBUG("%s: tensor %s needs %zu bytes (original %zu, extracted: weights=%zu scales=%zu zp=%zu)\n",
560            //                __func__, tensor->name, layout.total_size, ggml_nbytes(tensor), layout.weights_size,
561            //                layout.scales_size, layout.zp_size);
562            return layout.total_size;
563        }
564    }
565
566    return ggml_nbytes(tensor);
567}
568
569static const ggml_backend_buffer_type_i ggml_backend_openvino_buffer_type_interface = {
570    /* .get_name         = */ ggml_backend_openvino_buffer_type_get_name,
571    /* .alloc_buffer     = */ ggml_backend_openvino_buffer_type_alloc_buffer,
572    /* .get_alignment    = */ ggml_backend_openvino_buffer_type_get_alignment,
573    /* .get_max_size     = */ ggml_backend_openvino_buffer_type_get_max_size,
574    /* .get_alloc_size   = */ ggml_backend_openvino_buffer_type_get_alloc_size,
575    /* .is_host          = */ nullptr,
576};
577
578// Get buffer type for a specific device
579GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_buffer_type(int device) {
580    GGML_ASSERT(device >= 0 && device < ggml_backend_openvino_get_device_count());
581
582    static std::mutex mutex;
583    std::lock_guard<std::mutex> lock(mutex);
584
585    static std::vector<ggml_backend_buffer_type> buffer_types;
586    static std::vector<ggml_backend_openvino_buffer_type_context> buffer_type_contexts;
587
588    if (buffer_types.empty()) {
589        int device_count = ggml_backend_openvino_get_device_count();
590        buffer_types.resize(device_count);
591        buffer_type_contexts.resize(device_count);
592
593        for (int i = 0; i < device_count; i++) {
594            buffer_type_contexts[i].device = i;
595            buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i);
596
597            buffer_types[i] = ggml_backend_buffer_type{
598                /* .iface   = */ ggml_backend_openvino_buffer_type_interface,
599                /* .device  = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), i),
600                /* .context = */ &buffer_type_contexts[i],
601            };
602        }
603    }
604
605    return &buffer_types[device];
606}
607
608// =====================================================
609// OpenVINO Host Buffer Implementation
610// =====================================================
611
612static const char * ggml_backend_openvino_host_buffer_type_get_name(ggml_backend_buffer_type_t buft) {
613    ggml_backend_openvino_buffer_type_context * ctx = (ggml_backend_openvino_buffer_type_context *) buft->context;
614    static std::string name;
615    name = ctx->name + "_HOST";
616    return name.c_str();
617}
618
619static bool ggml_backend_openvino_host_buffer_type_is_host(ggml_backend_buffer_type_t buft) {
620    GGML_UNUSED(buft);
621    return true;
622}
623
624static const ggml_backend_buffer_type_i ggml_backend_openvino_host_buffer_type_interface = {
625    /* .get_name         = */ ggml_backend_openvino_host_buffer_type_get_name,
626    /* .alloc_buffer     = */ ggml_backend_openvino_buffer_type_alloc_buffer,
627    /* .get_alignment    = */ ggml_backend_openvino_buffer_type_get_alignment,
628    /* .get_max_size     = */ ggml_backend_openvino_buffer_type_get_max_size,
629    /* .get_alloc_size   = */ ggml_backend_openvino_buffer_type_get_alloc_size,
630    /* .is_host          = */ ggml_backend_openvino_host_buffer_type_is_host,
631};
632
633GGML_BACKEND_API ggml_backend_buffer_type_t ggml_backend_openvino_host_buffer_type(int device) {
634    GGML_ASSERT(device >= 0 && device < ggml_backend_openvino_get_device_count());
635
636    static std::mutex mutex;
637    std::lock_guard<std::mutex> lock(mutex);
638
639    static std::vector<ggml_backend_buffer_type> buffer_types;
640    static std::vector<ggml_backend_openvino_buffer_type_context> buffer_type_contexts;
641
642    if (buffer_types.empty()) {
643        int device_count = ggml_backend_openvino_get_device_count();
644        buffer_types.resize(device_count);
645        buffer_type_contexts.resize(device_count);
646
647        for (int i = 0; i < device_count; i++) {
648            buffer_type_contexts[i].device = i;
649            buffer_type_contexts[i].name = std::string(GGML_OPENVINO_NAME) + std::to_string(i);
650
651            buffer_types[i] = ggml_backend_buffer_type{
652                /* .iface   = */ ggml_backend_openvino_host_buffer_type_interface,
653                /* .device  = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), i),
654                /* .context = */ &buffer_type_contexts[i],
655            };
656        }
657    }
658
659    return &buffer_types[device];
660}
661
662bool ggml_backend_buffer_is_openvino(ggml_backend_buffer_t buffer) {
663    return buffer->iface.free_buffer == ggml_backend_openvino_buffer_free_buffer;
664}
665
666size_t ggml_backend_openvino_buffer_get_ctx_id(ggml_backend_buffer_t buffer) {
667    if (!ggml_backend_buffer_is_openvino(buffer)) {
668        return 0;
669    }
670    ggml_backend_openvino_buffer_context * ctx = (ggml_backend_openvino_buffer_context *) buffer->context;
671    return ctx->id;
672}
673
674bool ggml_openvino_buffer_is_remote(const ggml_tensor * tensor) {
675    if (tensor == nullptr || tensor->buffer == nullptr) {
676        return false;
677    }
678    if (!ggml_backend_buffer_is_openvino(tensor->buffer)) {
679        return false;
680    }
681    auto * ctx = static_cast<ggml_backend_openvino_buffer_context *>(tensor->buffer->context);
682    return ctx->is_remote;
683}
684
685void ggml_openvino_buffer_register_extra(ggml_tensor * tensor, ggml_openvino_extra_base * extra) {
686    GGML_ASSERT(tensor != nullptr);
687    GGML_ASSERT(tensor->buffer != nullptr);
688    GGML_ASSERT(ggml_backend_buffer_is_openvino(tensor->buffer));
689
690    auto * ctx = static_cast<ggml_backend_openvino_buffer_context *>(tensor->buffer->context);
691
692    auto it = ctx->tensor_extras.find(tensor);
693    if (it != ctx->tensor_extras.end()) {
694        delete it->second;
695    }
696
697    ctx->tensor_extras[tensor] = extra;
698    tensor->extra = extra;
699}
700
701bool ggml_backend_buft_is_openvino(ggml_backend_buffer_type_t buft) {
702    return buft->iface.get_name == ggml_backend_openvino_buffer_type_get_name;
703}
704
705bool ggml_backend_buft_is_openvino_host(ggml_backend_buffer_type_t buft) {
706    return buft->iface.get_name == ggml_backend_openvino_host_buffer_type_get_name;
707}
708
709static void ggml_backend_openvino_free(ggml_backend_t backend) {
710    ggml_backend_openvino_context * ctx = (ggml_backend_openvino_context *) backend->context;
711
712    if (ctx->runtime_context) {
713        auto r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context);
714        if (--r_ctx->backend_count == 0) {
715            // If host weight buffers were released (GGML_OPENVINO_RELEASE_WEIGHTS), the
716            // dropped pages can never be repopulated, so a recompile is impossible. Keep
717            // the compiled-model cache alive across backend teardown so the next context
718            // reuses it instead of recompiling against zeroed weights.
719            if (!ggml_openvino_weight_buffers_released()) {
720                r_ctx->clear_caches();
721            }
722        }
723    }
724
725    delete ctx;
726    delete backend;
727}
728
729static const char * ggml_backend_openvino_get_name(ggml_backend_t backend) {
730    return GGML_OPENVINO_NAME;
731    GGML_UNUSED(backend);
732}
733
734static enum ggml_status ggml_backend_openvino_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {
735    return ov_graph_compute(cgraph, backend);
736    GGML_UNUSED(backend);
737}
738
739static const ggml_backend_i ggml_backend_openvino_interface = {
740    /* .get_name                = */ ggml_backend_openvino_get_name,
741    /* .free                    = */ ggml_backend_openvino_free,
742    /* .set_tensor_async        = */ NULL,
743    /* .get_tensor_async        = */ NULL,
744    /* .set_tensor_2d_async     = */ NULL,
745    /* .get_tensor_2d_async     = */ NULL,
746    /* .cpy_tensor_async        = */ NULL,
747    /* .synchronize             = */ NULL,
748    /* .graph_plan_create       = */ NULL,
749    /* .graph_plan_free         = */ NULL,
750    /* .graph_plan_update       = */ NULL,
751    /* .graph_plan_compute      = */ NULL,
752    /* .graph_compute           = */ ggml_backend_openvino_graph_compute,
753    /* .event_record            = */ NULL,
754    /* .event_wait              = */ NULL,
755    /* .graph_optimize          = */ NULL,
756};
757
758int ggml_backend_openvino_get_device_count() {
759    return 1;
760}
761
762static ggml_guid_t ggml_backend_openvino_guid(void) {
763    static ggml_guid guid = {0x12, 0xa8, 0xae, 0xf4, 0xc0, 0x1e, 0x61, 0x97,
764                             0x8f, 0xeb, 0x33, 0x04, 0xa1, 0x33, 0x51, 0x2d};
765    return &guid;
766}
767
768static std::shared_ptr<ov_runtime_context> get_ov_runtime_context_ptr() {
769    static std::shared_ptr<ov_runtime_context> r_ctx = [] {
770        auto ctx = std::make_shared<ov_runtime_context>();
771        ctx->device = ggml_openvino_get_device_name();
772        ctx->stateful = is_stateful_enabled() && !ggml_openvino_is_npu();
773        return ctx;
774    }();
775    return r_ctx;
776}
777
778// backend API
779GGML_BACKEND_API ggml_backend_t ggml_backend_openvino_init(int device) {
780    if (device < 0 || device >= ggml_backend_openvino_get_device_count()) {
781        GGML_LOG_ERROR("%s: invalid device %d\n", __func__, device);
782        return nullptr;
783    }
784
785    ggml_backend_openvino_context * ctx = new ggml_backend_openvino_context;
786    if (ctx == nullptr) {
787        GGML_LOG_ERROR("%s: failed to allocate context\n", __func__);
788        return nullptr;
789    }
790
791    ctx->runtime_context = get_ov_runtime_context_ptr();
792    if (ctx->runtime_context == nullptr) {
793        GGML_LOG_ERROR("%s: failed to allocate runtime context\n", __func__);
794        delete ctx;
795        return nullptr;
796    }
797
798    std::shared_ptr<ov_runtime_context> r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context);
799    r_ctx->backend_count++;
800
801    ggml_backend_t openvino_backend = new ggml_backend{
802        /* .guid      = */ ggml_backend_openvino_guid(),
803        /* .interface = */ ggml_backend_openvino_interface,
804        /* .device    = */ ggml_backend_reg_dev_get(ggml_backend_openvino_reg(), device),
805        /* .context   = */ ctx,
806    };
807
808    return openvino_backend;
809}
810
811GGML_BACKEND_API bool ggml_backend_is_openvino(ggml_backend_t backend) {
812    return backend != NULL && ggml_guid_matches(backend->guid, ggml_backend_openvino_guid());
813}
814
815struct ggml_backend_openvino_device_context {
816    int device;
817    std::string name;
818    std::string description;
819};
820
821static const char * ggml_backend_openvino_device_get_name(ggml_backend_dev_t dev) {
822    ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context;
823    return ctx->name.c_str();
824}
825
826static const char * ggml_backend_openvino_device_get_description(ggml_backend_dev_t dev) {
827    ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context;
828    return ctx->description.c_str();
829}
830
831static void ggml_backend_openvino_device_get_memory(ggml_backend_dev_t dev, size_t * free, size_t * total) {
832#ifdef _WIN32
833    MEMORYSTATUSEX status;
834    status.dwLength = sizeof(status);
835    GlobalMemoryStatusEx(&status);
836    *total = status.ullTotalPhys;
837    *free = status.ullAvailPhys;
838#else
839    long pages = sysconf(_SC_PHYS_PAGES);
840    long page_size = sysconf(_SC_PAGE_SIZE);
841    *total = pages * page_size;
842
843    // "free" system memory is ill-defined, for practical purposes assume that all of it is free:
844    *free = *total;
845#endif  // _WIN32
846
847    GGML_UNUSED(dev);
848}
849
850static enum ggml_backend_dev_type ggml_backend_openvino_device_get_type(ggml_backend_dev_t dev) {
851    GGML_UNUSED(dev);
852    return GGML_BACKEND_DEVICE_TYPE_GPU;
853}
854
855static void ggml_backend_openvino_device_get_props(ggml_backend_dev_t dev, ggml_backend_dev_props * props) {
856    props->name = ggml_backend_openvino_device_get_name(dev);
857    props->description = ggml_backend_openvino_device_get_description(dev);
858    props->type = ggml_backend_openvino_device_get_type(dev);
859    ggml_backend_openvino_device_get_memory(dev, &props->memory_free, &props->memory_total);
860
861    props->caps = {
862        /* .async                 = */ false,
863        /* .host_buffer           = */ false,
864        /* .buffer_from_host_ptr  = */ false,
865        /* .events                = */ false,
866        /* .mmap_support          = */ true,
867    };
868}
869
870static ggml_backend_t ggml_backend_openvino_device_init(ggml_backend_dev_t dev, const char * params) {
871    GGML_UNUSED(params);
872    ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context;
873    return ggml_backend_openvino_init(ctx->device);
874}
875
876static ggml_backend_buffer_type_t ggml_backend_openvino_device_get_buffer_type(ggml_backend_dev_t dev) {
877    ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context;
878    return ggml_backend_openvino_buffer_type(ctx->device);
879}
880
881static ggml_backend_buffer_type_t ggml_backend_openvino_device_get_host_buffer_type(ggml_backend_dev_t dev) {
882    ggml_backend_openvino_device_context * ctx = (ggml_backend_openvino_device_context *) dev->context;
883    return ggml_backend_openvino_host_buffer_type(ctx->device);
884}
885
886static bool has_view_op_input(const ggml_tensor * op) {
887    for (int i = 0; i < GGML_MAX_SRC; i++) {
888        if (op->src[i] == nullptr) {
889            break;
890        }
891        if (op->src[i]->op == GGML_OP_VIEW) {
892            return true;
893        }
894    }
895    return false;
896}
897
898static bool has_non_contiguous_view_input(const ggml_tensor * op) {
899    for (int i = 0; i < GGML_MAX_SRC; i++) {
900        if (op->src[i] == nullptr) {
901            break;
902        }
903        if (op->src[i]->op == GGML_OP_VIEW && !ggml_is_contiguous(op->src[i])) {
904            return true;
905        }
906    }
907    return false;
908}
909
910static bool is_supported_flash_attn_pattern(const ggml_tensor * op) {
911    // Each Q/K/V input must follow one of:
912    //   PERMUTE -> VIEW  -> base (view_src==nullptr)   (llama KV-cache path)
913    //   PERMUTE -> RESHAPE -> base (view_src==nullptr)  (whisper Q)
914    //   VIEW -> base (view_src==nullptr)                (whisper K/V from kv_pad)
915    for (int i = 0; i < 3; i++) {
916        const ggml_tensor * src = op->src[i];
917        if (src->op == GGML_OP_PERMUTE) {
918            if (src->src[0] == nullptr) {
919                return false;
920            }
921            if (src->src[0]->op != GGML_OP_VIEW && src->src[0]->op != GGML_OP_RESHAPE) {
922                return false;
923            }
924            if (src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) {
925                return false;
926            }
927        } else if (src->op == GGML_OP_VIEW) {
928            if (src->src[0] == nullptr || src->src[0]->view_src != nullptr) {
929                return false;
930            }
931        } else {
932            return false;
933        }
934    }
935    return true;
936}
937
938static bool is_gemma3n_flash_attn_pattern(const ggml_tensor * op) {
939    if (!is_supported_flash_attn_pattern(op)) {
940        return false;
941    }
942
943    const ggml_tensor * q_base =
944        op->src[0] != nullptr && op->src[0]->src[0] != nullptr ? op->src[0]->src[0]->src[0] : nullptr;
945    const ggml_tensor * k_base =
946        op->src[1] != nullptr && op->src[1]->src[0] != nullptr ? op->src[1]->src[0]->src[0] : nullptr;
947    const ggml_tensor * v_base =
948        op->src[2] != nullptr && op->src[2]->src[0] != nullptr ? op->src[2]->src[0]->src[0] : nullptr;
949
950    if (q_base == nullptr || q_base->op != GGML_OP_ROPE) {
951        return false;
952    }
953
954    // gemma3n direct attention path (no KV cache): q=ROPE, k=ROPE, v=RMS_NORM
955    // Only match this specific pattern to avoid falsely catching other models
956    // (e.g. Gemma4) that also use scale=1.0 with KV-cache backed attention.
957    const bool is_qkv_direct =
958        k_base != nullptr && v_base != nullptr && k_base->op == GGML_OP_ROPE && v_base->op == GGML_OP_RMS_NORM;
959
960    return is_qkv_direct;
961}
962
963static bool checked_mul_size(size_t a, size_t b, size_t & out) {
964    if (a == 0 || b == 0) {
965        out = 0;
966        return true;
967    }
968    if (a > SIZE_MAX / b) {
969        return false;
970    }
971    out = a * b;
972    return true;
973}
974
975static bool tensor_view_fits_src_buffer(const ggml_tensor * tensor) {
976    if (tensor->view_src == nullptr) {
977        return true;
978    }
979
980    const size_t src_nbytes = ggml_nbytes(tensor->view_src);
981    if (tensor->view_offs > src_nbytes) {
982        return false;
983    }
984
985    const size_t tensor_nbytes = ggml_nbytes(tensor);
986    return tensor_nbytes <= src_nbytes - tensor->view_offs;
987}
988
989static bool cpy_output_view_is_supported(const ggml_tensor * op) {
990    if (op->view_src == nullptr) {
991        return true;
992    }
993
994    if (!tensor_view_fits_src_buffer(op)) {
995        return false;
996    }
997
998    return ggml_nbytes(op) == 0 || ggml_is_contiguous(op);
999}
1000
1001static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) {
1002    const ggml_tensor * as = op->src[0];
1003    const ggml_tensor * ids = op->src[2];
1004    if (as == nullptr || ids == nullptr) {
1005        return true;
1006    }
1007
1008    // The MXFP4 MUL_MAT_ID translation (translate_mul_mat_id_mxfp4_packed in mul_mat_id.cpp)
1009    // materializes selected expert weights with shape [n_tokens, n_used, rows, k]. Skip cases that
1010    // would create a very large temporary and let the scheduler fall back instead. Every other weight
1011    // type goes through GatherMatmul, which never materializes this temporary.
1012    size_t tmp_elems = 1;
1013    if (!checked_mul_size(tmp_elems, static_cast<size_t>(ids->ne[1]), tmp_elems) ||
1014        !checked_mul_size(tmp_elems, static_cast<size_t>(ids->ne[0]), tmp_elems) ||
1015        !checked_mul_size(tmp_elems, static_cast<size_t>(as->ne[1]), tmp_elems) ||
1016        !checked_mul_size(tmp_elems, static_cast<size_t>(as->ne[0]), tmp_elems)) {
1017        return true;
1018    }
1019
1020    size_t tmp_bytes = 0;
1021    if (!checked_mul_size(tmp_elems, sizeof(float), tmp_bytes)) {
1022        return true;
1023    }
1024
1025    static constexpr size_t mul_mat_id_tmp_limit = 1ULL << 30;  // 1 GiB
1026    return tmp_bytes > mul_mat_id_tmp_limit;
1027}
1028
1029static bool tensor_name_starts_with(const ggml_tensor * tensor, const char * prefix) {
1030    return tensor != nullptr && strncmp(tensor->name, prefix, strlen(prefix)) == 0;
1031}
1032
1033static bool is_msa_block_mask_expansion(const ggml_tensor * op) {
1034    if (tensor_name_starts_with(op, "msa_")) {
1035        return true;
1036    }
1037
1038    const ggml_tensor * src = op->src[0];
1039    while (src != nullptr && (src->op == GGML_OP_RESHAPE || src->op == GGML_OP_REPEAT)) {
1040        if (tensor_name_starts_with(src, "msa_block_mask")) {
1041            return true;
1042        }
1043        src = src->src[0];
1044    }
1045
1046    return tensor_name_starts_with(src, "msa_block_mask");
1047}
1048
1049namespace {
1050struct ggml_openvino_op_support {
1051    bool is_supported = true;
1052    std::string reason;
1053
1054    operator bool() const {
1055        return is_supported;
1056    }
1057};
1058} // namespace
1059
1060static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) {
1061    if (is_msa_block_mask_expansion(op)) {
1062        return {false, "MSA block mask expansion is not supported"};
1063    }
1064
1065    switch (op->op) {
1066    case GGML_OP_CONCAT: {
1067        if (op->type == GGML_TYPE_I64) {
1068            return {false, "CONCAT with I64 type is not supported"};
1069        }
1070        if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16 && has_view_op_input(op)) {
1071            return {false, "CONCAT with BF16 type and VIEW input is not supported on GPU"};
1072        }
1073        break;
1074    }
1075    case GGML_OP_SET: {
1076        const auto nb1 = static_cast<size_t>(op->op_params[0]);
1077        const auto nb2 = static_cast<size_t>(op->op_params[1]);
1078        const auto nb3 = static_cast<size_t>(op->op_params[2]);
1079
1080        // OpenVINO SET translation currently supports dst layouts that match src0 strides.
1081        if (op->src[0] == nullptr || nb1 != op->src[0]->nb[1] || nb2 != op->src[0]->nb[2] || nb3 != op->src[0]->nb[3]) {
1082            return {false, "SET op with dst nb1=" + std::to_string(nb1) + ", nb2=" + std::to_string(nb2) + ", nb3=" + std::to_string(nb3) +
1083                           " that does not match src0 strides nb[1]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[1]) : "null") +
1084                           ", nb[2]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[2]) : "null") +
1085                           ", nb[3]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[3]) : "null")};
1086        }
1087        break;
1088    }
1089    case GGML_OP_GET_ROWS:
1090    case GGML_OP_SET_ROWS: {
1091        if (op->ne[3] != 1) {
1092            return {false, "GET_ROWS/SET_ROWS with ne[3] != 1 (ne[3]=" + std::to_string(op->ne[3]) + ") is not supported"};
1093        }
1094        if (op->op == GGML_OP_GET_ROWS && ggml_is_quantized(op->src[0]->type) &&
1095            op->src[0]->view_src != nullptr && op->src[0]->view_offs != 0) {
1096            return {false, "GET_ROWS with a nonzero quantized src0 view offset is not supported"};
1097        }
1098        if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" &&
1099            op->src[0]->type == GGML_TYPE_BF16) {
1100            return {false, "GET_ROWS with BF16 src0 is not supported on GPU"};
1101        }
1102        if (op->ne[0] == 256 && (op->src[0]->type == GGML_TYPE_Q4_K || op->src[0]->type == GGML_TYPE_Q5_K ||
1103                                 op->src[0]->type == GGML_TYPE_Q4_1 || op->src[0]->type == GGML_TYPE_Q5_1)) {
1104            // These are all f16-arithmetic dequant rounding errors that intermittently exceed the
1105            // tight 1e-7 NMSE threshold depending on the random test data (see ggml-quants.cpp
1106            // make_int8_weights/make_int4_weights: dequant is done in f16, not f32, to keep the
1107            // Convert/Subtract/Multiply chain fusable into GatherMatmulCompressed/FullyConnectedCompressed
1108            // for the shared non-test code paths).
1109            return {false, "GET_ROWS/SET_ROWS with ne[0] == 256 and type " + std::string(ggml_type_name(op->src[0]->type)) +
1110                           " rejected due to f16-arithmetic dequant rounding errors that intermittently exceed 1e-7 NMSE threshold"};
1111        }
1112        break;
1113    }
1114    case GGML_OP_RESHAPE: {
1115        if (strncmp(op->name, "ffn_norm_exps", sizeof("ffn_norm_exps") - 1) == 0) {
1116            return {false, "RESHAPE for ffn_norm_exps is not supported"};
1117        }
1118        break;
1119    }
1120    case GGML_OP_ADD:
1121    case GGML_OP_MUL:
1122    case GGML_OP_SUB: {
1123        if (op->src[1]->op == GGML_OP_PERMUTE) {
1124            return {false, "ADD/MUL/SUB with PERMUTE src1 is not supported"};
1125        }
1126        for (int i = 0; i < 4; i++) {
1127            if (op->src[0]->ne[i] != op->src[1]->ne[i] && (op->src[0]->ne[i] != 1 && op->src[1]->ne[i] != 1)) {
1128                return {false, "ADD/MUL/SUB with incompatible broadcast shapes: src0->ne[" + std::to_string(i) + "]=" +
1129                               std::to_string(op->src[0]->ne[i]) + ", src1->ne[" + std::to_string(i) + "]=" +
1130                               std::to_string(op->src[1]->ne[i])};
1131            }
1132        }
1133        break;
1134    }
1135    case GGML_OP_ADD_ID: {
1136        // Keep support aligned with the CPU backend implementation, which only handles f32 inputs/output and i32 ids.
1137        if (op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32 || op->src[1]->type != GGML_TYPE_F32 ||
1138            op->src[2]->type != GGML_TYPE_I32) {
1139            return {false, "ADD_ID only supports F32 inputs/output and I32 ids"};
1140        }
1141        break;
1142    }
1143    case GGML_OP_DIV: {
1144        // The GPU plugin can fuse broadcast DIV into the preceding FFN GEMM path
1145        // and produce infs for per-channel scale vectors. Keep those DIVs on CPU
1146        // until the fused GPU kernel is reliable. (falied case llama-arch-test mpt)
1147        if (ggml_openvino_get_device_name() == "GPU" && op->src[1]->ne[0] == op->ne[0] &&
1148            op->src[1]->ne[1] == 1 && op->src[1]->ne[2] == 1 && op->src[1]->ne[3] == 1) {
1149            return {false, "DIV per-channel scale broadcast is not supported on GPU"};
1150        }
1151        break;
1152    }
1153    case GGML_OP_POOL_2D: {
1154        const auto& name = ggml_openvino_get_device_name();
1155        if (name == "GPU") {
1156            const int32_t * params = op->op_params;
1157            const int k0 = params[1];
1158            const int k1 = params[2];
1159            const int p0 = params[5];
1160            const int p1 = params[6];
1161            if ((p0 > 0 || p1 > 0) && (k0 < 3 || k1 < 3)) {
1162                return {false, "POOL_2D with padding and kernel size < 3 is not supported on " + name};
1163            }
1164        }
1165        break;
1166    }
1167    case GGML_OP_SUM_ROWS: {
1168        if (op->src[0]->op == GGML_OP_PERMUTE) {
1169            return {false, "SUM_ROWS with PERMUTE input is not supported"};
1170        }
1171        break;
1172    }
1173    case GGML_OP_FLASH_ATTN_EXT: {
1174        float scale = 1.0f;
1175        float max_bias = 0.0f;
1176        float logit_softcap = 0.0f;
1177        const auto * op_params = op->op_params;
1178        memcpy(&scale, (const float *) op_params + 0, sizeof(float));
1179        memcpy(&max_bias, (const float *) op_params + 1, sizeof(float));
1180        memcpy(&logit_softcap, (const float *) op_params + 2, sizeof(float));
1181
1182        // Keep gemma3n flash-attn pattern on CPU for GPU runs to avoid
1183        // accuracy drift in the OpenVINO path. Restrict by scale=1.0 to avoid
1184        // affecting non-gemma3n models such as Llama-3.2.
1185        if (fabsf(scale - 1.0f) < 1e-6f && is_gemma3n_flash_attn_pattern(op)) {
1186            return {false, "FLASH_ATTN_EXT gemma3n pattern on GPU is not supported"};
1187        }
1188
1189        if (op->src[4] != nullptr) {
1190            return {false, "FLASH_ATTN_EXT with sinks is not supported"};
1191        }
1192        if (!is_supported_flash_attn_pattern(op)) {
1193            return {false, "FLASH_ATTN_EXT unsupported attention pattern"};
1194        }
1195        if (max_bias > 0) {
1196            return {false, "FLASH_ATTN_EXT with max_bias > 0 (max_bias=" + std::to_string(max_bias) + ") is not supported"};
1197        }
1198        if (logit_softcap != 0) {
1199            return {false, "FLASH_ATTN_EXT with logit_softcap != 0 (logit_softcap=" + std::to_string(logit_softcap) + ") is not supported"};
1200        }
1201        break;
1202    }
1203    case GGML_OP_PERMUTE: {
1204        if (op->type == GGML_TYPE_BF16 && ggml_openvino_get_device_name() == "GPU") {
1205            return {false, "PERMUTE with BF16 type is not supported on GPU"};
1206        }
1207        break;
1208    }
1209    case GGML_OP_CPY: {
1210        if (op->src[0]->type == GGML_TYPE_BF16 || op->src[1]->type == GGML_TYPE_BF16) {
1211            return {false, "CPY with BF16 src type is not supported"};
1212        }
1213        // CPY to a quantized destination (e.g. f32 -> q4_0) is numerically unstable with OpenVINO backend.
1214        if (ggml_is_quantized(op->type)) {
1215            return {false, "CPY to quantized destination (e.g. f32 -> q4_0) is numerically unstable"};
1216        }
1217        if (ggml_nelements(op->src[0]) != ggml_nelements(op->src[1])) {
1218            return {false, "CPY with mismatched element counts is not supported: src0=" + std::to_string(ggml_nelements(op->src[0])) +
1219                           " != src1=" + std::to_string(ggml_nelements(op->src[1]))};
1220        }
1221        // op test case with non-contiguous src or dst
1222        if ((op->ne[0] == 3 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) ||
1223            (op->ne[0] == 1 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) ||
1224            (op->ne[0] == 2 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2)) {
1225            return {false, "CPY with non-contiguous shape [" + std::to_string(op->ne[0]) + ", " +
1226                           std::to_string(op->ne[1]) + ", " + std::to_string(op->ne[2]) + ", " +
1227                           std::to_string(op->ne[3]) + "] is not supported"};
1228        }
1229        if (!cpy_output_view_is_supported(op)) {
1230            return {false, "CPY with non-contiguous output view is not supported"};
1231        }
1232        break;
1233    }
1234    case GGML_OP_MUL_MAT: {
1235        if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[1] != nullptr &&
1236            ggml_is_quantized(op->src[0]->type) && strcmp(op->src[0]->name, "a") == 0 &&
1237            strcmp(op->src[1]->name, "b") == 0 && op->src[0]->ne[1] == 1 && op->src[1]->ne[1] == 64 &&
1238            op->src[0]->ne[0] == 256 && op->src[1]->ne[0] == 256) {
1239            return {false, "MUL_MAT quantized benchmark test case on GPU is not supported"};
1240        }
1241        if (op->src[0]->ne[3] != op->src[1]->ne[3] && op->src[0]->ne[3] != 1 && op->src[1]->ne[3] != 1) {
1242            return {false, "MUL_MAT with incompatible broadcast on ne[3]: src0->ne[3]=" + std::to_string(op->src[0]->ne[3]) +
1243                           ", src1->ne[3]=" + std::to_string(op->src[1]->ne[3])};
1244        }
1245        if (op->src[0]->op == GGML_OP_VIEW && op->src[1]->op == GGML_OP_VIEW) {
1246            return {false, "MUL_MAT with both inputs as VIEW is not supported"};
1247        }
1248        break;
1249    }
1250    case GGML_OP_MUL_MAT_ID: {
1251        // Single-expert (or empty) MUL_MAT_ID is a degenerate shape that stresses GatherMatmul edge
1252        // cases and never occurs in real MoE; let it fall back to CPU.
1253        if (op->src[0] != nullptr && op->src[0]->ne[2] <= 1) {
1254            return {false, "MUL_MAT_ID with single-expert or empty ne[2] <= 1 (ne[2]=" +
1255                           std::to_string(op->src[0]->ne[2]) + ") is not supported"};
1256        }
1257        if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) {
1258            return {false, "MUL_MAT_ID with BF16 weights on GPU is not supported"};
1259        }
1260        // GPU MUL_MAT_ID uses a Gather+MatMul fallback because the GPU plugin rejects internal
1261        // GatherMatmul for these test shapes. Skip cases that would materialize a large selected
1262        // expert-weight temporary.
1263        if (ggml_openvino_get_device_name() == "GPU" && mul_mat_id_requires_large_tmp(op)) {
1264            return {false, "MUL_MAT_ID requires large temporary on GPU"};
1265        }
1266        break;
1267    }
1268    case GGML_OP_ROPE: {
1269        const int32_t * op_params = op->op_params;
1270        const int n_dims = op_params[1];
1271        const int mode = op_params[2];
1272        if (op_params[15] != 0) {
1273            // FIXME: support ggml_rope_set_offset
1274            return {false, "ggml_rope_set_offset is not supported"};
1275        }
1276        if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX && mode != GGML_ROPE_TYPE_IMROPE) {
1277            return {false, "ROPE with mode " + std::to_string(mode) + " is not supported"};
1278        }
1279        const int64_t head_dim = op->src[0]->ne[0];
1280        const int64_t rope_dims = n_dims == 0 ? head_dim : n_dims;
1281        if (rope_dims <= 0 || rope_dims > head_dim || (rope_dims % 2) != 0) {
1282            return {false, "ROPE with n_dims=" + std::to_string(n_dims) + ", head_dim=" + std::to_string(head_dim) + " is not supported"};
1283        }
1284        if (op->type != GGML_TYPE_F32 && op->type != GGML_TYPE_F16) {
1285            return {false, "ROPE with type " + std::string(ggml_type_name(op->type)) + " is not supported"};
1286        }
1287        if (op->src[0]->op == GGML_OP_VIEW) {
1288            const struct ggml_tensor * view = op->src[0];
1289            const struct ggml_tensor * view_src = view->view_src;
1290            if (view_src->ne[1] != view->ne[1] || view_src->ne[2] != view->ne[2] || view_src->ne[3] != view->ne[3]) {
1291                return {false, "ROPE with view_src->ne [" + std::to_string(view_src->ne[1]) + ", " +
1292                               std::to_string(view_src->ne[2]) + ", " + std::to_string(view_src->ne[3]) +
1293                               "] != view->ne [" + std::to_string(view->ne[1]) + ", " +
1294                               std::to_string(view->ne[2]) + ", " + std::to_string(view->ne[3]) +
1295                               "] is not supported"};
1296            }
1297        }
1298        if (mode == GGML_ROPE_TYPE_IMROPE &&
1299            (op->src[2] != 0 || ((const float *) op_params)[6] != 1 || ((const float *) op_params)[7] != 0 ||
1300             ((const float *) op_params)[8] != 1)) {
1301            return {false, "IMROPE with freq_factors, freq_scale, ext_factor, and attn_factor is not supported"};
1302        }
1303        break;
1304    }
1305    case GGML_OP_TRANSPOSE: {
1306        if (op->type == GGML_TYPE_BF16) {
1307            return {false, "TRANSPOSE with BF16 type is not supported"};
1308        }
1309        break;
1310    }
1311    case GGML_OP_REPEAT: {
1312        if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16) {
1313            return {false, "REPEAT with BF16 type is not supported on GPU"};
1314        }
1315        break;
1316    }
1317    case GGML_OP_GATED_DELTA_NET: {
1318        // enable after https://github.com/openvinotoolkit/openvino/pull/35917 is included in OV release
1319        // return true;
1320        // if (ggml_openvino_get_device_name() == "GPU" && op->src[0]->ne[2] > 1) {
1321        //     // CVS-186471
1322        //     return true;
1323        // }
1324        if (op->src[2]->op == GGML_OP_PERMUTE) {
1325            return {false, "GATED_DELTA_NET with PERMUTE src2 is not supported"};
1326        }
1327        // kda (per-key-dimension gating) not supported by fused GatedDeltaNet op
1328        if (op->src[3]->ne[0] != 1) {
1329            return {false, "GATED_DELTA_NET with kda (per-key-dimension gating) is not supported"};
1330        }
1331        // K > 1 (multiple state snapshots) not supported by fused op
1332        if (((const int32_t *) op->op_params)[0] > 1) {
1333            return {false, "GATED_DELTA_NET with K > 1 (multiple state snapshots) is not supported"};
1334        }
1335        break;
1336    }
1337    case GGML_OP_SSM_CONV: {
1338        // qwen3next is numerically unstable with OpenVINO SSM_CONV.
1339        // Keep this op on CPU until the OpenVINO implementation is fixed.
1340        // return true;
1341        break;
1342    }
1343    case GGML_OP_VIEW: {
1344        // Skip TOPK_MOE fused tests until it is fully supported.
1345        // The argsort_top_k VIEW wrapping ARGSORT is named "selected_experts" in test_topk_moe.
1346        if (strcmp(op->name, "selected_experts") == 0) {
1347            return {false, "VIEW for selected_experts (argsort_top_k) is not supported"};
1348        }
1349        break;
1350    }
1351    default:
1352        break;
1353    }
1354    return {true, ""};
1355}
1356
1357static ggml_openvino_op_support ggml_backend_openvino_device_supports_op_impl(ggml_backend_dev_t dev, const ggml_tensor * op) {
1358    GGML_ASSERT(dev->reg != nullptr);
1359
1360    static std::unordered_set<ggml_type> supported_types{
1361        GGML_TYPE_F32,  GGML_TYPE_F16,  GGML_TYPE_BF16, GGML_TYPE_I64,  GGML_TYPE_I32,  GGML_TYPE_Q4_0,
1362        GGML_TYPE_Q4_1, GGML_TYPE_Q4_K, GGML_TYPE_Q5_1, GGML_TYPE_Q5_K, GGML_TYPE_Q8_0, GGML_TYPE_Q6_K,
1363        GGML_TYPE_MXFP4};
1364
1365    // derive supported op sets from the op_table map, keys in
1366    // the map use the full macro name (e.g. "GGML_OP_ADD"), while
1367    // the ggml_*_op_name() helpers return only the trailing part (e.g. "ADD").
1368    // each set is built once and cached.
1369    static const auto build_supported_sets = [] {
1370        const auto & table = ov::frontend::ggml::get_supported_ops();
1371        std::unordered_set<ggml_op> ops;
1372        std::unordered_set<ggml_unary_op> unary_ops;
1373        std::unordered_set<ggml_glu_op> glu_ops;
1374
1375        // GGML_OP_NONE has no translator but is always safe to add to the supported set.
1376        ops.insert(GGML_OP_NONE);
1377
1378        for (int i = 0; i < GGML_OP_COUNT; ++i) {
1379            const std::string key = std::string("GGML_OP_") + ggml_op_name(static_cast<ggml_op>(i));
1380            if (table.count(key)) {
1381                ops.insert(static_cast<ggml_op>(i));
1382            }
1383        }
1384        for (int i = 0; i < GGML_UNARY_OP_COUNT; ++i) {
1385            const std::string key = std::string("GGML_UNARY_OP_") + ggml_unary_op_name(static_cast<ggml_unary_op>(i));
1386            if (table.count(key)) {
1387                unary_ops.insert(static_cast<ggml_unary_op>(i));
1388            }
1389        }
1390        for (int i = 0; i < GGML_GLU_OP_COUNT; ++i) {
1391            const std::string key = std::string("GGML_GLU_OP_") + ggml_glu_op_name(static_cast<ggml_glu_op>(i));
1392            if (table.count(key)) {
1393                glu_ops.insert(static_cast<ggml_glu_op>(i));
1394            }
1395        }
1396        return std::make_tuple(ops, unary_ops, glu_ops);
1397    };
1398    static const auto supported_sets = build_supported_sets();
1399    static const auto & supported_ops = std::get<0>(supported_sets);
1400    static const auto & supported_unary_ops = std::get<1>(supported_sets);
1401    static const auto & supported_glu_ops = std::get<2>(supported_sets);
1402
1403    switch (op->op) {
1404    case GGML_OP_UNARY: {
1405        auto supported = supported_unary_ops.find(ggml_get_unary_op(op)) != supported_unary_ops.end();
1406        if (!supported) {
1407            return {false, "unary op " + std::string(ggml_unary_op_name(ggml_get_unary_op(op))) + " has no op translator"};
1408        }
1409        if (ggml_get_unary_op(op) == GGML_UNARY_OP_EXP && op->type == GGML_TYPE_F32) {
1410            return {false, "UNARY_EXP with F32 type is not supported"};
1411        }
1412        break;
1413    }
1414    case GGML_OP_GLU: {
1415        auto supported = supported_glu_ops.find(ggml_get_glu_op(op)) != supported_glu_ops.end();
1416        if (!supported) {
1417            return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " has no op translator"};
1418        }
1419        // if (has_view_op_input(op)) {
1420        //     return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " with view input is not supported"};
1421        // }
1422        if (op->src[1] == nullptr && op->src[0]->ne[0] % 2 != 0) {
1423            // triggers bug in ov gpu
1424            return {false, "GLU op with odd src0 ne[0] and null src1 is not supported"};
1425        }
1426        break;
1427    }
1428    default: {
1429        auto supported = supported_ops.find(op->op) != supported_ops.end();
1430        if (!supported) {
1431            return {false, "op " + std::string(ggml_op_name(op->op)) + " has no op translator"};
1432        }
1433        static std::set<ggml_op> ops_not_support_view_input{};
1434        if (ops_not_support_view_input.find(op->op) != ops_not_support_view_input.end() && has_view_op_input(op)) {
1435            return {false, "op " + std::string(ggml_op_name(op->op)) + " with VIEW input is not supported"};
1436        }
1437    }
1438    }
1439
1440    if (supported_types.find(op->type) == supported_types.end()) {
1441        return {false, "tensor type " + std::string(ggml_type_name(op->type)) + " is not supported"};
1442    }
1443    for (int i = 0; i < GGML_MAX_SRC; i++) {
1444        auto * src = op->src[i];
1445        if (src == nullptr) {
1446            break;
1447        }
1448        if (supported_types.find(src->type) == supported_types.end()) {
1449            return {false, "src[" + std::to_string(i) + "] type " + std::string(ggml_type_name(src->type)) + " is not supported"};
1450        }
1451        const bool is_supported_3d_moe_expert =
1452            op->op == GGML_OP_MUL_MAT_ID && i == 0 && (src->type == GGML_TYPE_MXFP4 || src->ne[3] == 1);
1453        if (ggml_is_quantized(src->type) && src->ne[2] != 1 && !is_supported_3d_moe_expert) {
1454            return {false, "3D quantized tensor for src[" + std::to_string(i) + "] is not supported"};
1455        }
1456    }
1457
1458    auto op_support_case = is_op_supported_case(op);
1459    if (!op_support_case.is_supported) {
1460        return op_support_case;
1461    }
1462    return {true, ""};
1463}
1464
1465static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, const ggml_tensor * op) {
1466    auto res = ggml_backend_openvino_device_supports_op_impl(dev, op);
1467    if (!res.is_supported) {
1468        static const bool log_unsupported = ggml_openvino_getenv_int("GGML_OPENVINO_LOG_UNSUPPORTED_OPS") != 0;
1469        if (log_unsupported) {
1470            GGML_LOG_WARN("OpenVINO op unsupported: op '%s' (%s), type %s: %s\n",
1471                          op->name, ggml_op_name(op->op), ggml_type_name(op->type), res.reason.c_str());
1472        }
1473    }
1474    return res.is_supported;
1475}
1476
1477static bool ggml_backend_openvino_device_supports_buft(ggml_backend_dev_t dev, ggml_backend_buffer_type_t buft) {
1478    return ggml_backend_buft_is_openvino(buft) || ggml_backend_buft_is_host(buft);
1479    GGML_UNUSED(dev);
1480}
1481
1482static const struct ggml_backend_device_i ggml_backend_openvino_device_interface = {
1483    /* .get_name             = */ ggml_backend_openvino_device_get_name,
1484    /* .get_description      = */ ggml_backend_openvino_device_get_description,
1485    /* .get_memory           = */ ggml_backend_openvino_device_get_memory,
1486    /* .get_type             = */ ggml_backend_openvino_device_get_type,
1487    /* .get_props            = */ ggml_backend_openvino_device_get_props,
1488    /* .init_backend         = */ ggml_backend_openvino_device_init,
1489    /* .get_buffer_type      = */ ggml_backend_openvino_device_get_buffer_type,
1490    /* .get_host_buffer_type = */ ggml_backend_openvino_device_get_host_buffer_type,
1491    /* .buffer_from_host_ptr = */ NULL,
1492    /* .supports_op          = */ ggml_backend_openvino_device_supports_op,
1493    /* .supports_buft        = */ ggml_backend_openvino_device_supports_buft,
1494    /* .offload_op           = */ NULL,
1495    /* .event_new            = */ NULL,
1496    /* .event_free           = */ NULL,
1497    /* .event_synchronize    = */ NULL,
1498};
1499
1500struct ggml_backend_openvino_reg_context {
1501    std::vector<ggml_backend_dev_t> devices;
1502};
1503
1504static const char * ggml_backend_openvino_reg_get_name(ggml_backend_reg_t reg) {
1505    return GGML_OPENVINO_NAME;
1506    GGML_UNUSED(reg);
1507}
1508
1509static size_t ggml_backend_openvino_reg_get_device_count(ggml_backend_reg_t reg) {
1510    GGML_UNUSED(reg);
1511    return (size_t) ggml_backend_openvino_get_device_count();
1512}
1513
1514static ggml_backend_dev_t ggml_backend_openvino_reg_get_device(ggml_backend_reg_t reg, size_t index) {
1515    ggml_backend_openvino_reg_context * ctx = (ggml_backend_openvino_reg_context *) reg->context;
1516    GGML_ASSERT(index < ctx->devices.size());
1517    return ctx->devices[index];
1518}
1519
1520static const struct ggml_backend_reg_i ggml_backend_openvino_reg_interface = {
1521    /* .get_name         = */ ggml_backend_openvino_reg_get_name,
1522    /* .get_device_count = */ ggml_backend_openvino_reg_get_device_count,
1523    /* .get_device       = */ ggml_backend_openvino_reg_get_device,
1524    /* .get_proc_address = */ NULL,
1525};
1526
1527static void ggml_openvino_init() {
1528    // Initialize device config singleton from env var
1529    ggml_openvino_init_device_config();
1530    GGML_LOG_INFO("OpenVINO: using device %s\n", ggml_openvino_get_device_name().c_str());
1531}
1532
1533GGML_BACKEND_API ggml_backend_reg_t ggml_backend_openvino_reg(void) {
1534    static ggml_backend_reg reg;
1535
1536    static bool initialized = false;
1537    {
1538        static std::mutex mutex;
1539        std::lock_guard<std::mutex> lock(mutex);
1540        if (!initialized) {
1541            ggml_openvino_init();
1542
1543            ggml_backend_openvino_reg_context * ctx = new ggml_backend_openvino_reg_context;
1544
1545            for (int i = 0; i < ggml_backend_openvino_get_device_count(); i++) {
1546                ggml_backend_openvino_device_context * dev_ctx = new ggml_backend_openvino_device_context;
1547                dev_ctx->device = i;
1548                dev_ctx->name = GGML_OPENVINO_NAME + std::to_string(i);
1549
1550                dev_ctx->description = ov::get_openvino_version().description;
1551
1552                ggml_backend_dev_t dev =
1553                    new ggml_backend_device{/* .interface = */ ggml_backend_openvino_device_interface,
1554                                            /* .reg       = */ &reg,
1555                                            /* .context   = */ dev_ctx};
1556                ctx->devices.push_back(dev);
1557            }
1558
1559            reg = ggml_backend_reg{/* .api_version = */ GGML_BACKEND_API_VERSION,
1560                                   /* .iface       = */ ggml_backend_openvino_reg_interface,
1561                                   /* .context     = */ ctx};
1562        }
1563
1564        initialized = true;
1565    }
1566
1567    return &reg;
1568}
1569
1570GGML_BACKEND_DL_IMPL(ggml_backend_openvino_reg)