Compare commits

...
17 Commits
Author SHA1 Message Date
Aman Gupta a4ddf1e6cf fix split 2026-08-14 21:10:38 +08:00
Aman Gupta 5cc9e2a911 rpc : allow -sm tensor on RDMA enabled devices
- add internal all reduce
- add SET_TENSOR_2D/GET_TENSOR_2D for strided transfers
- add LRU graph cache
2026-08-14 21:10:38 +08:00
Aman Gupta 7c26c91500 set coarser granularity for head splits 2026-08-14 21:10:38 +08:00
Aman Gupta 7b07e05c1e DSV4: sm tensor 2026-08-14 21:10:38 +08:00
Georgi GerganovandGitHub 4c1a0af40d llama : allow virtual igpu devices (#26953)
* llama : allow virtual igpu devices

* cont : better comment
2026-08-14 15:14:19 +03:00
Xuan-Son NguyenandGitHub 77918caf30 server: allow accessing /metrics and /slots during llama_decode() (#27041)
* server_queue::worker

* call llama_decode inside yield_to_queue

* also handle process_mtmd_chunk

* clean up

* nits

* rm test
2026-08-14 13:23:10 +02:00
885c5bbe8e tests : replace personal home directory paths with generic placeholders (#27043)
Scrub developer-specific /home/<user>/ paths from example docs and test
fixtures so they don't leak into the tree.

- examples/test-cmake/README.md: /home/danbev/... -> /path/to/llama.cpp/...
- tests/test-chat.cpp: /home/jarvis/... -> /home/user/... (input and
  expected string kept identical so the parser test still passes)

Co-authored-by: Jim Wu <ywu@xilinx.com>
2026-08-14 10:32:59 +02:00
TitaniumtownandGitHub 6509138622 sycl: fuse mul_mat(gate) + mul_mat(up) + GLU for q4_K dense FFN (#26779)
Measured on Arc Pro B70 (Battlemage, Level Zero), llama-bench -r 20, two
interleaved rounds, tg128:

    qwen2.5-3B-Instruct Q4_K_M    154.18 -> 158.53 t/s   +2.8%
    gemma-2-2b-it Q4_K_M          162.45 -> 165.62 t/s   +2.0%

llama-batched-bench on qwen2.5-3B, S_TG by batch size:

      B=1   142.72 -> 147.57 t/s    +3.4%
      B=2   243.72 -> 268.26 t/s   +10.1%
      B=4   359.58 -> 398.02 t/s   +10.7%
      B=8   449.75 -> 505.63 t/s   +12.4%
2026-08-14 02:26:23 -04:00
Mendy BergerandGitHub c6f6a92c55 ggml: force single thread on wasi (#25686) 2026-08-14 09:16:20 +03:00
TitaniumtownandGitHub 3d93885352 sycl: fuse the gated-delta-net state writeback cpy (#26643)
Port of https://github.com/ggml-org/llama.cpp/pull/23940.

Arc Pro B70, Qwen 3.6 27B Q4_K - Medium (48 of its 64 blocks run
gated_delta_net), -ngl 99 -fa 1 -ctk f16 -ctv f16 -b 2048 -ub 2048,
interleaved A/B passes of r=3:

  tg128           23.91 / 23.90 / 23.90 -> 24.19 / 24.17 / 24.20   +1.2%
  tg128 (rebuild) 23.81 / 23.81         -> 24.09 / 24.10           +1.2%
  pp2048        1050.8  / 1053.9        -> 1053.8 / 1054.5         flat
  2 seqs, tg128    32.73 / 32.75        ->  33.11 / 33.10          +1.1%
2026-08-14 09:00:36 +03:00
Daniel BeveniusandGitHub 2bacf9ea5c dflash : clarify output logging of target_layer_ids (#27013)
This commit tries to make the logging of target_layer_ids a bit clearer
and easier to read.

Currently the output generated looks like this:
```console
0.00.468.624 D load_arch_hparams: DFlash extract_layers = [0.00.468.626 D 2, 0.00.468.626 D 6, 0.00.468.626 D 20,
  0.00.468.626 D 30, 0.00.468.627 D 42, 0.00.468.627 D 520.00.468.627 D ]
```
With the changes in the commit the output will be:
```console
0.00.522.765 D load_arch_hparams: DFlash extract_layers = [2, 6, 20, 30, 42, 52]
```
2026-08-14 06:57:05 +02:00
Niklas WenzelandGitHub a94d563ed8 common: apply CPU parameters across tools (#27026) 2026-08-13 20:40:59 +02:00
Aleksander GrygierandGitHub bdffafa5df ui: Refactor data-attrs constants, enum for bool strings (#27002)
* refactor: Data-attribute constants + boolean string enum

* refactor: Use CSS class string constants

* refactor: Address review comments
2026-08-13 20:01:12 +02:00
Aleksander GrygierandGitHub fa4ec4590c refactor: Naming (#27001) 2026-08-13 19:45:32 +02:00
IlyaandGitHub 9c5531e2bf ui: fix VITE_PUBLIC_SERVER variable reading (#24845) 2026-08-13 19:31:51 +02:00
aee56b3abf OpenVINO: Qwen3.5, memory optimization, and test-recurrent-state-rollback (#26952)
* OpenVINO backend: 1) enable gpt-oss moe on OV bk; 2) enable mxfp4 support

* OpenVINO backend: disable TOPK_MOE op test

* OpenVINO Backend: Add op FILL support

* OpenVINO backend: enable set rows with multi dims

* fix the name missmatch in setrow + view

* OpenVINO backend: enable op GGML_UNARY_OP_SIGMOID

* OpenVINO Backend: enable SQR & SQRT

* OpenVINO backend: 1) ensure unique node names for OpenVINO; 2) add org_src to recorde the src ggml tensor for OpenVINO dynamic shape infer

* OpenVINO backend: enable fallback for openVINO to CPU backend

* OpenVINO backend: fix accurace issue in gemma3n arch test

* fix mpt failed case

* OpenVINO backend: clean nodeinfo

* OpenVINO Backend: enable zero-size copy for view

* add concat ssm_conv in compute_dynamic_dim

enable qwen35

Fix after rebase

remove logging

* OpenVINO backend: disable EXP with FP32, which failed in op test. Root reason: the backend test initializes unary op inputs over a wide range, [-150, 150]. For FP32, exp(x) overflows around x ~= 88.7, so this test can randomly generate values right in or beyond the overflow region

* OpenVINO backend: fix CPY op test failed issue

* OpenVINO backend: fix GATED_DELTA_NET op test failed issue

* handle in-place op, handle qwen35 dynamic clearing of cache in cgraph

* handle qwen35 dynamic clearing of cache correctly

* Enable qwen35 dense multi seq

* Fix qwen35 9b gqa

* Fix after rebase

* Disable SOLVE_TRI

* openvino: fix NEOX RoPE accuracy on GPU stateful (mixed-rank Multiply)

In stateful mode the NEOX RoPE branch fed rank-3 data ([S, n_heads,
head_size]) into the Multiply against the rank-4 cos/sin tables
([1, S, 1, n_dims/2]). That mixed-rank broadcast is miscomputed by the
OpenVINO GPU plugin, corrupting the rotated Q/K and producing garbage
output (e.g. Phi-3-mini). Lift the data to rank-4 before the split/
Multiply so the operands are equal-rank, matching what the TYPE_NORMAL
branch already does. CPU and stateless paths are unaffected.

Phi-3-mini-Q4_K_M, wiki.test perplexity, GPU stateful:
  before: PPL = 27120.43
  after:  PPL = 6.2263   (CPU reference: 6.2251)

* OpenVINO backend: 1) remove the unique name in llama.cpp; 2) add new ov name in ov bk; 3) fix issue in arch test & op test with latest code update

* OpenVINO Backenb: remove changes in llama.cpp

* Doc change (use x64 Native Tools Command Prompt for VS)

* Cleaner sentence

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* OpenVINO Backend: cache key upgrade includes all src name

* OpenVINO Backend: enable llama arch test on ci

* OpenVINO Backend: move parameter node creating from decoder into translate

* OpenVINO Backend: create extra input ov node move from decoder to translate

* fix for op regression due to is_model_splitted

* openvino: fix CPY writeback for recurrent state rollback

Detect the rollback conv/gdn state writeback CPY nodes structurally
instead of by tensor name, since the rollback path in
build_conv_state does not call cb() and left the nodes unnamed. Add
per-node runtime offsets (rs_slot_begin_*, rs_src_begin_*) so the
cached IR handles any kv head, sequence count and snapshot slot for
both the conv state and the GDN state writeback.

Assisted-by: GitHub Copilot

* qwen35 moe

* optimize MoE expert aggregation with ReduceSum

* Skip GET_ROWS inaccurate test

* openvino: fallback dynamic MUL_MAT_ID shapes

* OpenVINO Backend: fix error in arch test model mpt

* fix error caused by cpy in arch test model kimi-linear

* OpenVINO Backend: fix error in arch test model minimax-m3

* openvino: fix GPU mul_mat_id op tests

* ggml-openvino: add GGML_OPENVINO_RELEASE_WEIGHTS to reclaim host weight RSS on GPU

The OpenVINO weight Constants are zero-copy views into host buffers
allocated by the backend (ggml_aligned_malloc, anonymous memory). On GPU
the plugin holds its own device copy after compile_model, so these host
pages are dead weight for inference. For a 1B Q4_K_M model this leaves
~850 MB of host RSS resident that the GPU path never reads again.

Add an opt-in GGML_OPENVINO_RELEASE_WEIGHTS mode that madvise(MADV_DONTNEED)s
the registered host weight buffers once the model is compiled, dropping
their resident pages while keeping the mappings valid (ggml still owns the
lifetime; tensors still point in). Measured steady-state RSS drops from
~1555 MB to ~710 MB on Llama-3.2-1B-Q4_K_M (Arc iGPU) with unchanged
throughput and correct output.

The GPU backend uses a single dynamic-shape model for both prefill and
decode, so a graph is compiled once and reused; the only event that forces
a recompile is clear_caches() on backend teardown. The change therefore:
  - releases on the first cache-hit (model compiled, plugin has its copy);
  - pins the compiled-model cache across backend teardown so a later
    context reuses it instead of recompiling against the dropped pages;
  - fails loud (GGML_ABORT) on a cache-miss recompile or on a second model
    load, both of which would otherwise read zeroed weights or silently
    reuse the wrong compiled graph.

Scope/limitations (all fail loud, never silently wrong): GPU only (the CPU
plugin reads the host Constants at inference time), one model per process,
and stable graph shapes. This reduces steady-state RSS, not the transient
compile-time peak. All changes are confined to the OpenVINO backend.

* ggml-openvino: stream weight requantization to cut the compile-time RSS peak

requantize_to_buffers() dequantized the entire tensor to a temporary
std::vector<float> of n_elements before requantizing. For token_embd.weight
(128256 x 2048) that transient is ~1 GB (1B model) / ~2 GB (8B), and it is
the single largest contributor to the OpenVINO compile-time memory peak --
it also fires twice for token_embd (once at load, once at graph build,
because token_embd is loaded via a CPU/mmap buffer and not cached as an OV
weight extra).

Stream the dequant instead: process a fixed window of complete rows
(CHUNK_ROWS=256) into a small scratch buffer and quantize/convert each chunk
straight into the output buffers. The transient F32 footprint is now
CHUNK_ROWS*ne0 floats regardless of tensor size.

quantize_q8_0/q8_1 gain an optional block_offset arg (default 0) so a chunk
writes its weights/scales/zp at the correct block. Streaming is applied to
the Q8_0_C / Q8_1_C / F16 targets (the large requant cases); the u4 (Q4_0)
path keeps the whole-array call because it packs two weights per byte with
running zp ORs, and a fallback handles any future target whose block size
does not divide a row.

Measured peak RSS (cold compile, GPU): 1B 2868 -> 1809 MB (-1.06 GB);
8B 11618 -> 9608 MB (-2.0 GB). Output verified unchanged
("capital of France is Paris"); throughput unchanged. Unlike
GGML_OPENVINO_RELEASE_WEIGHTS this reduces the transient peak, not just
steady-state, and needs no env flag. All changes confined to the OpenVINO
backend.

* ggml-openvino: avoid redundant token_embd requantization at compile

token_embd.weight is referenced twice in the graph path: as the GET_ROWS
embedding (a CPU/mmap-buffer tensor) it was re-extracted/re-requantized on
every weight-node build, and is_model_splitted() built a full (naive) set of
weight nodes just to test name membership — each requant is a ~1-2 GB F32
dequant of the 262M-element embedding.

Two changes:
- Add collect_weight_names(): a name-only collector for topology checks.
  is_model_splitted() now uses it instead of create_weight_nodes(cgraph,
  true), so the splitted-check no longer triggers any weight extraction.
- Memoize weight nodes built from non-OpenVINO buffers in a process-lifetime
  cache keyed by tensor->data. These tensors have no OV buffer context to own
  a cached extra, so without this they were rebuilt on every (re)compile;
  prefill and decode graphs now share one build (verified: 2nd graph hits the
  cache instead of re-requantizing).

Peak RSS is unchanged (the streaming-requant commit already removed the F32
transient); this removes redundant compile-time work. Output verified
unchanged ("capital of France is Paris"). Confined to the OpenVINO backend.

* ggml-openvino: gate compile-memory optimizations behind GGML_OPENVINO_REDUCE_COMPILE_MEM

The streaming requantization and the non-OpenVINO-buffer weight-node cache
(plus the name-only is_model_splitted path that pairs with it) are now opt-in
via GGML_OPENVINO_REDUCE_COMPILE_MEM. When unset, requantize_to_buffers()
fully materializes the F32 buffer and weights are rebuilt per compile exactly
as before; when set, the streaming path and the cross-compile weight cache
are used.

Default off keeps behavior identical to upstream unless explicitly enabled.
Verified: flag off -> peak RSS 2800 MB (original), flag on -> 1810 MB; output
"capital of France is Paris" in both modes. (GGML_OPENVINO_RELEASE_WEIGHTS,
added earlier, remains a separate opt-in for the steady-state release.)

* ggml-openvino: add frontend model cache (GGML_OPENVINO_MODEL_CACHE_DIR)

The plugin-level ov::cache_dir caches the compiled blob keyed by the OV
model, but producing that model still runs the full frontend every time:
weight requantization (incl. the large token_embd F32 transient) and the
ggml->OV graph conversion. This adds an opt-in frontend cache keyed off a
fingerprint computed directly from the ggml cgraph, so a hit imports a
previously exported CompiledModel and skips requant + convert + compile
entirely.

Key (model-cache.{h,cpp}) = 64-bit FNV-1a of: graph topology (n_nodes + per
node op/name), a sampled per-weight fingerprint (name/shape/type + bounded
head+tail byte sample), and blob-affecting config (device, flash-attn, rope
params, REDUCE_COMPILE_MEM/stateful flags, OpenVINO version). A sidecar
manifest stores every weight's fingerprint and is re-verified on load, so a
sampled-hash collision cannot cause a wrong-model hit (verified: two
different quantizations of the same model produce distinct cache entries).

Flow (dynamic single-model path only; split models defer to ov::cache_dir):
on a verified hit, core.import_model() restores the CompiledModel and a
lightweight decoder is built with a names-only weight map (membership is all
the decoder needs for I/O mapping; weights live in the imported model). On a
miss, compile as usual then export the blob (atomic temp+rename, manifest
written first). The frontend cache supersedes ov::cache_dir, so CACHE_DIR/
CACHE_MODE are stripped from the config used for the cached compile and the
import — a blob compiled with cache_dir set cannot be re-imported.

Measured 8B Q4_K_M (GPU): full requant+convert+compile 15.3s -> import 6.3s
(~2.4x faster compile phase). Output verified unchanged on cold and warm,
standalone and combined with REDUCE_COMPILE_MEM + RELEASE_WEIGHTS. Default
off; confined to the OpenVINO backend.

* ggml-openvino: harden frontend model cache correctness

The frontend model cache imports a previously exported CompiledModel keyed by a fingerprint of the ggml graph, weights, and blob-affecting config. The original key covered device, stateful execution, REDUCE_COMPILE_MEM, RoPE params, OpenVINO version, topology, and sampled weights, but missed runtime/frontend toggles that can change the lowered graph or the I/O binding contract. That made it possible to reuse a blob produced under a different OpenVINO backend configuration.

Add a small extra-config helper for the dynamic model-cache path and fold in the effective values of GGML_OPENVINO_DISABLE_KV_SLICE and GGML_OPENVINO_MANUAL_GQA_ATTN. MANUAL_GQA_ATTN is keyed by the behavior that actually takes effect: an explicit env value wins, otherwise GPU defaults to enabled and other devices default to disabled. This matches flash_attn_ext lowering and avoids unnecessary cache splits for equivalent configurations while separating genuinely different attention graphs.

DISABLE_KV_SLICE is also included because it changes the KV-cache tensor shape/output binding strategy used around imported models. Even when weights and graph topology are identical, switching this flag should not inherit a CompiledModel cache entry created for a different binding mode.

Also make cache artifact publication cleaner: write manifest.tmp and blob.tmp, publish the blob first, and publish the manifest last. Cache hits already require both blob and a verified manifest, so making the manifest the final visible artifact avoids leaving an apparently complete manifest for a failed or interrupted blob export. Temporary files are removed on the handled failure paths.

While touching this path, fix the indentation of the non-imported compile branch so the cache miss flow is easier to review. Behavior is otherwise unchanged: verified hits still import, misses still create weights, convert, compile, export, and create the infer request normally.

* ggml-openvino: add memory optimization umbrella switch

Add GGML_OPENVINO_MEMORY_OPTIMIZE as a single opt-in switch for the OpenVINO backend memory-saving paths. The existing fine-grained GGML_OPENVINO_REDUCE_COMPILE_MEM and GGML_OPENVINO_RELEASE_WEIGHTS variables remain supported and explicitly override the umbrella switch when set, so users can still bisect or disable one side of the optimization independently.

Centralize the policy in ggml_openvino_reduce_compile_mem_enabled() and ggml_openvino_release_weights_enabled(device). The umbrella switch enables compile-memory reductions everywhere REDUCE_COMPILE_MEM is used today: streaming requantization, non-OV weight-node caching, split-model weight-name collection, and the frontend model-cache fingerprint. On GPU it also enables host weight-buffer release unless GGML_OPENVINO_RELEASE_WEIGHTS is explicitly set.

Keep host weight release GPU-only because it relies on the plugin holding its own device copy after compile_model. Update the fail-fast diagnostic and comments to mention GGML_OPENVINO_MEMORY_OPTIMIZE, so users who enable the umbrella switch get accurate guidance if a later cache-miss recompile would read released host weight pages.

* ggml-openvino: rename compiled model cache env

Rename the frontend export/import cache environment variable from GGML_OPENVINO_MODEL_CACHE_DIR to GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR. The cache stores blobs produced by ov::CompiledModel::export_model() and restores them with core.import_model(), so the new name distinguishes it from GGML_OPENVINO_CACHE_DIR, which configures OpenVINO plugin-level ov::cache_dir.

Update the registered env var, the cache-directory lookup, and comments around the frontend compiled-model cache. The old GGML_OPENVINO_MODEL_CACHE_DIR name is removed rather than kept as a fallback so there is a single spelling for the new option.

* docs: document OpenVINO memory optimization env vars

Add runtime configuration entries for the newly recognized OpenVINO environment variables.

Document GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR as the frontend compiled-model cache used to export and import compiled blobs for matching single-graph models.

Document GGML_OPENVINO_MEMORY_OPTIMIZE as the umbrella switch, including how GGML_OPENVINO_REDUCE_COMPILE_MEM and the GPU-only GGML_OPENVINO_RELEASE_WEIGHTS override or inherit from it.

* ggml-openvino: fix Qwen3VL crash and deepstack correctness bug

1. GGML_OP_PAD was missing from compute_node_dynamic_dims(), causing a crash
on decode for models that pad the token embedding (n_embd -> n_embd_inp).
PAD never reorders/merges dims, so it keeps the same dynamic dim index as
its source.

2. process_view_input_new() chained VIEW inputs through src[0] (the
immediate op-graph parent) using offsets treated as relative to that
parent. But ggml_tensor::view_offs is always absolute from the true root
allocation (ggml collapses VIEW-of-VIEW chains internally). For the
per-layer deepstack view ("embd (view)", whose src[0] is "embd" - itself
an already-narrowed, zero-offset VIEW of the padded root, with the SAME
ggml shape as the deepstack view but a different absolute offset), this
caused an out-of-bounds re-slice that silently fell back to returning the
wrong (already-resolved sibling) tensor. In practice every deepstack ADD
ended up adding the real base token embedding into the residual stream
instead of zero, corrupting generation ("Hello my name is 1000000..."
instead of coherent text). Fixed by detecting this pattern (same shape as
the immediate src, different absolute offset) and re-slicing directly
from the untouched root tensor using the innermost view's absolute
offset.

Also adds a GGML_OPENVINO_DEBUG_NODE=<name1>,<name2>,... env var that attaches
extra debug Result nodes for arbitrary intermediate tensors, without binding
them to any ggml buffer (avoiding the risk of reading a ggml buffer that has
since been overwritten by a later in-place op). This was instrumental in
diagnosing bug #2 above and is left in as a general-purpose debugging aid.

* ggml-openvino: fix IMROPE inp_pos padding for NPU static shapes

IMROPE's inp_pos tensor packs 4 stacked t/h/w/e position planes into
ne[0] = 4*n_tokens instead of one value per token. On NPU's static-shape
path, inp_pos was padded/shaped as if it held a single plane, which
interleaved padding across the 4 planes and desynced later reshapes
from the rest of the (chunk_size-wide) graph.

- add GgmlOvDecoder::get_inp_pos_n_planes() to detect IMROPE's 4-plane layout
- get_graph_input_shape(): size inp_pos as n_planes * chunk_size (prefill)
  or n_planes (decode) instead of assuming 1 value per token
- get_ov_input_tensor_static_prefill(): pad each plane to chunk_size
  independently instead of one flat block
- get_ov_input_tensor_static_decode(): copy n_planes contiguous values
  instead of asserting/copying a single scalar

* disable test-llama-archs tests.

* openvino: gate fallback with env var

* Revert changes in test-llama-archs

* Apply editor config

* reject CPY with quantized destination as unsupported

---------

Co-authored-by: Xuejun <Xuejun.Zhai@intel.com>
Co-authored-by: Mustafa Cavus <mustafa.cavus@intel.com>
Co-authored-by: virajwad <84867530+virajwad@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: suryasidd <surya.siddharth.pemmaraju@intel.com>
Co-authored-by: Mustafa Cavus <mustafacavus@intel.com>
Co-authored-by: Ravi Panchumarthy <ravi.panchumarthy@intel.com>
2026-08-13 20:30:48 +03:00
Neo ZhangandGitHub a97123e497 [SYCL] Support host pinned mem to improve SYCL Host-to-Device Memory Access (#26789)
* support host pinned mem, ggml_backend_sycl_host_buffer_type_get_max_size,

* fix the thread-safe issue
2026-08-13 20:05:33 +03:00
121 changed files with 5976 additions and 1328 deletions
+60
View File
@@ -1275,6 +1275,8 @@ struct common_init_result::impl {
// note: the order in which model, context, etc. are declared matters because their destructors will be called bottom-to-top
common_threadpools threadpools;
llama_model_ptr model;
llama_context_ptr context;
@@ -1376,6 +1378,10 @@ common_init_result::common_init_result(common_params & params, bool model_only)
}
pimpl->context.reset(lctx);
set_process_priority(params.cpuparams.priority);
pimpl->threadpools.init(lctx, params);
}
llama_model * common_init_result::model() {
@@ -1724,6 +1730,10 @@ struct llama_context_params common_context_params_to_llama(const common_params &
return cparams;
}
//
// Threadpool utils
//
struct ggml_threadpool_params ggml_threadpool_params_from_cpu_params(const common_cpu_params & params) {
struct ggml_threadpool_params tpp;
@@ -1740,6 +1750,56 @@ struct ggml_threadpool_params ggml_threadpool_params_from_cpu_params(const commo
return tpp;
}
common_threadpools::~common_threadpools() {
if (!free_fn) {
return;
}
free_fn(threadpool);
free_fn(threadpool_batch);
}
void common_threadpools::init(llama_context * ctx, const common_params & params) {
GGML_ASSERT(!threadpool);
GGML_ASSERT(!threadpool_batch);
COM_INF("llama threadpool init, n_threads = %d\n", (int) params.cpuparams.n_threads);
auto * cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
if (!cpu_dev) {
COM_WRN("%s", "no CPU backend found\n");
return;
}
auto * reg = ggml_backend_dev_backend_reg(cpu_dev);
auto * ggml_threadpool_new_fn = (decltype(ggml_threadpool_new) *) ggml_backend_reg_get_proc_address(reg, "ggml_threadpool_new");
free_fn = (decltype(ggml_threadpool_free) *) ggml_backend_reg_get_proc_address(reg, "ggml_threadpool_free");
struct ggml_threadpool_params tpp_batch =
ggml_threadpool_params_from_cpu_params(params.cpuparams_batch);
struct ggml_threadpool_params tpp =
ggml_threadpool_params_from_cpu_params(params.cpuparams);
if (!ggml_threadpool_params_match(&tpp, &tpp_batch)) {
threadpool_batch = ggml_threadpool_new_fn(&tpp_batch);
if (!threadpool_batch) {
COM_WRN("batch threadpool create failed : n_threads %d\n", tpp_batch.n_threads);
return;
}
// start the non-batch threadpool in the paused state
tpp.paused = true;
}
threadpool = ggml_threadpool_new_fn(&tpp);
if (!threadpool) {
COM_WRN("threadpool create failed : n_threads %d\n", tpp.n_threads);
free_fn(threadpool_batch);
threadpool_batch = nullptr;
return;
}
llama_attach_threadpool(ctx, threadpool, threadpool_batch);
}
//
// Batch utils
//
+24 -3
View File
@@ -929,9 +929,8 @@ using common_init_result_ptr = std::unique_ptr<common_init_result>;
common_init_result_ptr common_init_from_params(common_params & params, bool model_only = false);
struct llama_model_params common_model_params_to_llama ( common_params & params);
struct llama_context_params common_context_params_to_llama(const common_params & params);
struct ggml_threadpool_params ggml_threadpool_params_from_cpu_params(const common_cpu_params & params);
struct llama_model_params common_model_params_to_llama ( common_params & params);
struct llama_context_params common_context_params_to_llama(const common_params & params);
// clear LoRA adapters from context, then apply new list of adapters
void common_set_adapter_lora(struct llama_context * ctx, std::vector<common_adapter_lora_info> & lora);
@@ -942,6 +941,28 @@ std::string common_get_model_endpoint();
// for testing purposes
char * common_get_model_or_exit(int, char*[]);
//
// Threadpool utils
//
struct ggml_threadpool_params ggml_threadpool_params_from_cpu_params(const common_cpu_params & params);
struct common_threadpools {
common_threadpools() = default;
~common_threadpools();
common_threadpools(const common_threadpools &) = delete;
common_threadpools & operator=(const common_threadpools &) = delete;
void init(llama_context * ctx, const common_params & params);
private:
ggml_threadpool * threadpool = nullptr;
ggml_threadpool * threadpool_batch = nullptr;
decltype(ggml_threadpool_free) * free_fn = nullptr;
};
//
// Context utils
//
+5 -1
View File
@@ -206,7 +206,7 @@ cmake -B build/ReleaseOV -G Ninja -DCMAKE_BUILD_TYPE=Release -DGGML_OPENVINO=ON
cmake --build build/ReleaseOV --parallel
```
- **Windows:** Open a **Developer Command Prompt for VS 2022** (so the MSVC toolchain is on `PATH`), then run:
- **Windows:** Open **x64 Native Tools Command Prompt for VS** (so the MSVC toolchain is on `PATH`), then run:
```cmd
C:\Intel\openvino\setupvars.bat
@@ -710,11 +710,15 @@ Boolean flags follow a uniform convention: set to a **positive integer** (e.g. `
|-----------------------------------|-----------|------------|-------------------------------------------------------------------------------------------------------------|
| `GGML_OPENVINO_DEVICE` | String | `CPU` | Specify the target device (CPU, GPU, NPU). On systems with multiple GPUs, use `GPU.0` or `GPU.1` to explicitly target specific GPU. See [OpenVINO GPU Device](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/gpu-device.html). When set to **NPU**, static compilation mode is enabled for optimal performance. |
| `GGML_OPENVINO_CACHE_DIR` | String | `not set` | Directory for OpenVINO model caching (recommended: `/tmp/ov_cache`). Enables model caching when set. **Not supported on NPU devices.** |
| `GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR` | String | `not set` | Directory for the frontend compiled-model cache. When set, OpenVINO compiled models are exported as blobs and imported on later runs to skip weight requantization, graph conversion, and compilation for matching single-graph models. |
| `GGML_OPENVINO_PREFILL_CHUNK_SIZE`| Integer | `256` | Token chunk size for **NPU** prefill (NPU-only; ignored on CPU/GPU). Must be a positive integer; otherwise the default is used. |
| `GGML_OPENVINO_STATEFUL_EXECUTION`| Boolean | `0` | Enable stateful KV cache for better performance. Recommended on CPU, GPU. |
| `GGML_OPENVINO_DISABLE_CACHE` | Boolean | `0` | Disable the in-process compiled-model / decoder cache (cache is on by default). Set to `1` to disable. |
| `GGML_OPENVINO_DISABLE_KV_SLICE` | Boolean | `0` | Disable the KV-cache input-tensor slicing optimization (slicing is on by default on CPU/GPU). Set to `1` to disable. |
| `GGML_OPENVINO_MANUAL_GQA_ATTN` | Boolean | device-based | Tri-state. When **unset**, manual GQA attention is enabled by default on `GPU` and disabled on other devices. Set to a positive integer to force-enable, or `0` to force-disable. |
| `GGML_OPENVINO_MEMORY_OPTIMIZE` | Boolean | `0` | Umbrella switch for compile-time memory reductions. Enables `GGML_OPENVINO_REDUCE_COMPILE_MEM` and, on GPU, `GGML_OPENVINO_RELEASE_WEIGHTS` unless those fine-grained variables are explicitly set. |
| `GGML_OPENVINO_REDUCE_COMPILE_MEM`| Boolean | inherits from `GGML_OPENVINO_MEMORY_OPTIMIZE` | Reduce compile-time host memory use by streaming weight requantization and avoiding extra weight-node materialization where possible. Set explicitly to override the umbrella switch. |
| `GGML_OPENVINO_RELEASE_WEIGHTS` | Boolean | inherits from `GGML_OPENVINO_MEMORY_OPTIMIZE` on GPU | GPU-only. Release host weight buffers after the compiled model cache can reuse the device/plugin copy. Requires stable graph shapes; dynamic workloads that need recompilation should leave this disabled. |
| `GGML_OPENVINO_PROFILING` | Boolean | `0` | Enable execution-time profiling. |
| `GGML_OPENVINO_DUMP_CGRAPH` | Boolean | `0` | Dump the GGML compute graph to `cgraph_ov.txt`. |
| `GGML_OPENVINO_DUMP_IR` | Boolean | `0` | Serialize OpenVINO IR files with timestamps. |
+2 -1
View File
@@ -795,6 +795,7 @@ User can use the device management in [docs/multi-gpu.md](https://github.com/ggm
| GGML_SYCL_ENABLE_FLASH_ATTN | 1 (default) or 0| Enable Flash-Attention. It can reduce memory usage. The performance impact depends on the LLM.|
| GGML_SYCL_ENABLE_OPT | 0 or 1 (default)| Enable optimize features for Intel GPUs. (Recommended to 0 for Intel devices older than Gen 10) |
| GGML_SYCL_ENABLE_GRAPH | 0 (default) or 1 | Enable running computations through SYCL Graphs feature. Disabled by default because SYCL Graph is still on development, no better performance. |
| GGML_SYCL_ENABLE_HOST_PINNED_MEM | 0 or 1 (default) | Enable host pinned memory to speed up copy data from host to device. When disable it, host memory will common malloc() on CPU.|
| GGML_SYCL_USE_LEVEL_ZERO_API | 1 (default) or 0 | Use Level Zero API for device memory allocation instead of SYCL. Reduces system RAM usage on Intel dGPUs by avoiding DMA-buf/TTM host memory staging. Requires GGML_SYCL_SUPPORT_LEVEL_ZERO_API=ON at build time. SYCL backend always runs on Level Zero running time even if it's set as OFF (The SYCL api will be usage for memory allocation).|
| GGML_SYCL_ENABLE_DNN | 0 or 1 (default)| Enable running computations through oneDNN and always use oneMKL. |
| GGML_SYCL_FA_ONEDNN | 1 (default) or 0 | Enable the oneDNN fused SDPA (flash-attention) path on supported GPUs. Set to 0 to always use the native SYCL flash-attention kernel. |
@@ -803,7 +804,7 @@ User can use the device management in [docs/multi-gpu.md](https://github.com/ggm
| GGML_SYCL_ENABLE_MKL_FA | 1 (default) or 0 | Enable oneMKL GEMM flash attention for XMX-accelerated prompt processing with quantized KV cache. Automatically activates during prefill (prompt processing) when all conditions are met: (1) flash-attn enabled (`-fa` or `--flash-attn on`), (2) KV cache quantized (`--cache-type-k q8_0 --cache-type-v q8_0` or other `*_0/*_1` types), (3) batch size ≥ 1024 (`--batch-size 1024`), (4) prompt length ≥ 1024 tokens. Set to 0 to force the TILE kernel for A/B testing. Example minimum command: `llama-cli -m model.gguf -fa -ngl 99 --cache-type-k q8_0 --cache-type-v q8_0 --batch-size 1024 -p "your prompt"` |
| GGML_SYCL_MKL_FA_DEBUG | 0 (default) or 1 | Enable per-call diagnostic logging for MKL flash attention: GEMM/softmax timings, interleaved-head detection, and buffer memory usage. |
| GGML_SYCL_MKL_FA_DIAG | 0 (default) or 1 | Enable output fingerprinting for MKL flash attention. Dumps the first 64 float output values for the first 6 FA calls with n_kv ≥ 1024, labeled with kernel type (MKL/TILE/VEC) for cross-kernel comparison. |
| GGML_SYCL_ENABLE_FUSION | 0 or 1 (default) | Enable fused-kernel dispatch in graph compute (currently top-k MoE gating). |
| GGML_SYCL_ENABLE_FUSION | 0 or 1 (default) | Enable fused-kernel dispatch in graph compute. |
| GGML_SYCL_ENABLE_ESIMD | 0 or 1 (default)| Enable ESIMD kernels when available. |
| ZES_ENABLE_SYSMAN | 0 (default) or 1 | Support to get free memory of GPU by sycl::aspect::ext_intel_free_memory.<br>Recommended to use when --split-mode = layer |
| UR_L0_ENABLE_RELAXED_ALLOCATION_LIMITS | 0 (default) or 1 | Allow SYCL/Unified Runtime Level Zero device allocations larger than 4 GiB. llama.cpp's direct Level Zero allocation path requests the relaxed maximum-size limit itself when GGML_SYCL_ENABLE_LEVEL_ZERO=1. |
+2 -2
View File
@@ -27,10 +27,10 @@ Build/run this project using the installation created above:
(venv) $ ./build.sh
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/danbev/work/ai/llama.cpp/examples/test-cmake/build
-- Build files have been written to: /path/to/llama.cpp/examples/test-cmake/build
[100%] Built target test-cmake
[test-cmake] Using llama.cpp version 0.1.0-dev-b10335
[test-cmake] Initializing backend...
load_backend: loaded CPU backend from /home/danbev/work/ai/llama.cpp/examples/test-cmake/install/lib/llama.cpp/libggml-cpu-alderlake.so
load_backend: loaded CPU backend from /path/to/llama.cpp/examples/test-cmake/install/lib/llama.cpp/libggml-cpu-alderlake.so
[test-cmake] Backend initialized.
```
+2 -2
View File
@@ -6,8 +6,8 @@
extern "C" {
#endif
#define RPC_PROTO_MAJOR_VERSION 5
#define RPC_PROTO_MINOR_VERSION 0
#define RPC_PROTO_MAJOR_VERSION 6
#define RPC_PROTO_MINOR_VERSION 1
#define RPC_PROTO_PATCH_VERSION 0
#ifdef __cplusplus
+159 -10
View File
@@ -592,7 +592,18 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
GGML_ASSERT(split_states_equal(src_ss[0], src_ss[1]));
return {assume_sync ? GGML_BACKEND_SPLIT_AXIS_MIRRORED : GGML_BACKEND_SPLIT_AXIS_PARTIAL, {0}, {1}, 1};
}
GGML_ABORT("fatal error");
if (src_ss[0].axis == src_ss[1].axis && src_ss[0].axis >= GGML_BACKEND_SPLIT_AXIS_2 &&
src_ss[0].axis < GGML_MAX_DIMS) {
GGML_ASSERT(split_states_equal(src_ss[0], src_ss[1]));
return src_ss[0];
}
// batched matmul with the batches split across devices and a replicated activation
if (src_ss[0].axis >= GGML_BACKEND_SPLIT_AXIS_2 && src_ss[0].axis < GGML_MAX_DIMS &&
src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED) {
return src_ss[0];
}
GGML_ABORT("unsupported mul_mat split states: node=%s src0=%s axis=%d src1=%s axis=%d",
tensor->name, tensor->src[0]->name, (int) src_ss[0].axis, tensor->src[1]->name, (int) src_ss[1].axis);
//return {GGML_BACKEND_SPLIT_AXIS_UNKNOWN, {0}, {1}, 1};
};
@@ -747,14 +758,33 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
};
auto handle_flash_attn_ext = [&](const std::vector<ggml_backend_meta_split_state> & src_ss) -> ggml_backend_meta_split_state {
GGML_ASSERT( src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_2);
GGML_ASSERT( src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_2);
GGML_ASSERT( src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_2);
GGML_ASSERT(tensor->src[4] == nullptr || src_ss[3].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
GGML_ASSERT(tensor->src[3] == nullptr || src_ss[3].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
if (src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED) {
GGML_ASSERT(src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
GGML_ASSERT(src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
GGML_ASSERT(tensor->src[4] == nullptr || src_ss[4].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
return {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1};
}
GGML_ASSERT(src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_2);
const bool kv_split = src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_2 &&
src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_2;
const bool kv_mirrored = src_ss[1].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED &&
src_ss[2].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED;
GGML_ASSERT(kv_split || kv_mirrored);
GGML_ASSERT(tensor->src[4] == nullptr || src_ss[4].axis == GGML_BACKEND_SPLIT_AXIS_0);
return {GGML_BACKEND_SPLIT_AXIS_1, {0}, {1}, 1};
};
auto handle_lightning_indexer = [&](
const std::vector<ggml_backend_meta_split_state> & src_ss) -> ggml_backend_meta_split_state {
for (size_t i = 0; i < 4; i++) {
GGML_ASSERT(src_ss[i].axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
}
return {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1};
};
auto handle_ssm_conv = [&](const std::vector<ggml_backend_meta_split_state> & src_ss) -> ggml_backend_meta_split_state {
if (src_ss[0].axis == src_ss[1].axis) {
if (src_ss[0].axis == GGML_BACKEND_SPLIT_AXIS_0) {
@@ -819,7 +849,12 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
ggml_backend_meta_split_state split_state;
switch (tensor->op) {
case GGML_OP_NONE: {
split_state = {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1};
if (tensor->view_src != nullptr) {
// full-tensor view created with ggml_view_tensor, transparent for the split state
split_state = ggml_backend_meta_get_split_state(stc, tensor->view_src, assume_sync);
} else {
split_state = {GGML_BACKEND_SPLIT_AXIS_MIRRORED, {0}, {1}, 1};
}
} break;
case GGML_OP_DUP: {
split_state = handle_generic(src_ss, /*scalar_only =*/ true);
@@ -922,7 +957,7 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
split_state = handle_rope(src_ss);
} break;
case GGML_OP_ROPE_BACK: {
split_state = handle_generic(src_ss, /*scalar_only =*/ true);
split_state = handle_rope(src_ss);
} break;
case GGML_OP_CLAMP: {
split_state = handle_generic(src_ss, /*scalar_only =*/ false);
@@ -986,6 +1021,9 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
case GGML_OP_GATED_DELTA_NET: {
split_state = handle_gated_delta_net(src_ss);
} break;
case GGML_OP_LIGHTNING_INDEXER: {
split_state = handle_lightning_indexer(src_ss);
} break;
case GGML_OP_DSV4_HC_COMB:
case GGML_OP_DSV4_HC_PRE:
case GGML_OP_DSV4_HC_POST: {
@@ -1070,13 +1108,14 @@ static struct ggml_backend_meta_split_state ggml_backend_meta_get_split_state(
if (buf_ctx->debug > 0) {
std::string srcs_info;
for (size_t i = 0; i < GGML_MAX_SRC; i++) {
if (tensor->src[i] == nullptr) {
if (tensor->src[i] == nullptr || tensor->src[i] == tensor) {
continue;
}
if (!srcs_info.empty()) {
srcs_info += ", ";
}
const ggml_backend_meta_split_state split_state = ggml_backend_meta_get_split_state(tensor->src[0], true);
const ggml_backend_meta_split_state split_state =
ggml_backend_meta_get_split_state(tensor->src[i], true);
GGML_ASSERT(split_state.n_segments == 1);
const char * axis_name = ggml_backend_meta_split_axis_name(split_state.axis);
std::string ne_info;
@@ -1255,6 +1294,108 @@ static enum ggml_status ggml_backend_meta_buffer_init_tensor(ggml_backend_buffer
return ggml_backend_meta_buffer_init_tensor_impl(buf_ctx->get_simple_tensor_container(tensor), tensor);
}
static void ggml_backend_meta_buffer_memset_tensor(
ggml_backend_buffer_t buffer, ggml_tensor * tensor, uint8_t value, size_t offset, size_t size) {
const size_t n_bufs = ggml_backend_meta_buffer_n_bufs(buffer);
const ggml_backend_meta_split_state split_state =
ggml_backend_meta_get_split_state(tensor, /*assume_sync =*/ false);
GGML_ASSERT(ggml_is_contiguous(tensor) || split_state.axis == GGML_BACKEND_SPLIT_AXIS_MIRRORED);
if (split_state.n_segments != 1 || split_state.nr[0] != 1) {
GGML_ASSERT(split_state.axis >= 0 && split_state.axis < GGML_MAX_DIMS);
GGML_ASSERT(split_state.nr[0] != 0);
GGML_ASSERT(tensor->ne[3] == 1);
std::vector<size_t> simple_offsets(n_bufs, 0);
if (split_state.axis == GGML_BACKEND_SPLIT_AXIS_0) {
GGML_ASSERT(tensor->ne[2] == 1);
const size_t row_stride = tensor->nb[1];
GGML_ASSERT(offset % row_stride == 0);
GGML_ASSERT(size % row_stride == 0);
const int64_t row_start = offset / row_stride;
const int64_t row_count = size / row_stride;
GGML_ASSERT(row_start + row_count <= tensor->ne[1]);
const int64_t blck_size = ggml_blck_size(tensor->type);
for (size_t s = 0; s < split_state.n_segments; s++) {
for (size_t r = 0; r < split_state.nr[s]; r++) {
for (size_t j = 0; j < n_bufs; j++) {
ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j);
GGML_ASSERT(split_state.ne[s*n_bufs + j] % blck_size == 0);
const size_t nbytes = split_state.ne[s*n_bufs + j]/blck_size * tensor->nb[0];
for (int64_t row = 0; row < row_count; row++) {
ggml_backend_tensor_memset(simple_tensor, value,
simple_offsets[j] + (row_start + row)*simple_tensor->nb[1], nbytes);
}
simple_offsets[j] += nbytes;
}
}
}
return;
}
GGML_ASSERT(split_state.axis == GGML_BACKEND_SPLIT_AXIS_1);
const size_t row_stride = tensor->nb[2];
GGML_ASSERT(offset % row_stride == 0);
GGML_ASSERT(size % row_stride == 0);
const int64_t row_start = offset / row_stride;
const int64_t row_count = size / row_stride;
GGML_ASSERT(row_start + row_count <= tensor->ne[2]);
for (size_t s = 0; s < split_state.n_segments; s++) {
for (size_t r = 0; r < split_state.nr[s]; r++) {
for (size_t j = 0; j < n_bufs; j++) {
ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j);
const size_t nbytes = split_state.ne[s*n_bufs + j] * tensor->nb[1];
for (int64_t row = 0; row < row_count; row++) {
ggml_backend_tensor_memset(simple_tensor, value,
simple_offsets[j] + (row_start + row)*simple_tensor->nb[2], nbytes);
}
simple_offsets[j] += nbytes;
}
}
}
return;
}
switch (split_state.axis) {
case GGML_BACKEND_SPLIT_AXIS_0:
case GGML_BACKEND_SPLIT_AXIS_1:
case GGML_BACKEND_SPLIT_AXIS_2: {
const size_t chunk_size_full = tensor->nb[split_state.axis + 1];
GGML_ASSERT(offset % chunk_size_full == 0);
GGML_ASSERT(size % chunk_size_full == 0);
const int64_t i_start = offset / chunk_size_full;
const int64_t i_stop = (offset + size) / chunk_size_full;
for (size_t j = 0; j < n_bufs; j++) {
ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j);
const size_t chunk_size = simple_tensor->nb[split_state.axis + 1];
if (chunk_size == 0) {
continue;
}
for (int64_t i = i_start; i < i_stop; i++) {
ggml_backend_tensor_memset(simple_tensor, value, i*chunk_size, chunk_size);
}
}
} break;
case GGML_BACKEND_SPLIT_AXIS_PARTIAL: {
GGML_ASSERT(value == 0);
[[fallthrough]];
}
case GGML_BACKEND_SPLIT_AXIS_MIRRORED: {
for (size_t j = 0; j < n_bufs; j++) {
ggml_tensor * simple_tensor = ggml_backend_meta_buffer_simple_tensor(tensor, j);
ggml_backend_tensor_memset(simple_tensor, value, offset, size);
}
} break;
default: {
GGML_ABORT("fatal error");
}
}
}
static void ggml_backend_meta_buffer_set_tensor(ggml_backend_buffer_t buffer, ggml_tensor * tensor, const void * data, size_t offset, size_t size) {
const size_t n_bufs = ggml_backend_meta_buffer_n_bufs(buffer);
const ggml_backend_meta_split_state split_state = ggml_backend_meta_get_split_state(tensor, /*assume_sync =*/ false);
@@ -1488,7 +1629,7 @@ static const ggml_backend_buffer_i ggml_backend_meta_buffer_iface = {
/* .free_buffer = */ ggml_backend_meta_buffer_free_buffer,
/* .get_base = */ ggml_backend_meta_buffer_get_base,
/* .init_tensor = */ ggml_backend_meta_buffer_init_tensor,
/* .memset_tensor = */ nullptr, // TODO implement
/* .memset_tensor = */ ggml_backend_meta_buffer_memset_tensor,
/* .set_tensor = */ ggml_backend_meta_buffer_set_tensor,
/* .get_tensor = */ ggml_backend_meta_buffer_get_tensor,
/* .set_tensor_2d = */ nullptr,
@@ -2045,6 +2186,14 @@ static enum ggml_status ggml_backend_meta_graph_compute(ggml_backend_t backend,
cgraph_ij->uid = ggml_graph_next_uid();
}
}
// Aux graph contents are rewritten on every compute but are identical across calls while the subgraphs are reused,
// so they can get stable uids on rebuild. Only safe without a comm backend, where the fallback usage is deterministic.
if (backend_ctx->comm_ctx == nullptr) {
for (ggml_cgraph * cgraph_aux : backend_ctx->cgraphs_aux) {
cgraph_aux->uid = ggml_graph_next_uid();
}
}
}
size_t iga = 0; // i graph aux
+5
View File
@@ -2795,6 +2795,11 @@ struct ggml_cplan ggml_graph_plan(
n_threads = 1;
#endif
#if defined(__wasi__)
// WASI doesn't support parallelism yet
n_threads = 1;
#endif
size_t work_size = 0;
struct ggml_cplan cplan;
+423 -73
View File
@@ -16,6 +16,7 @@
#include <iomanip>
#include <map>
#include <memory>
#include <mutex>
#include <openvino/core/dimension.hpp>
#include <openvino/core/except.hpp>
#include <openvino/core/node.hpp>
@@ -25,12 +26,13 @@
#include <openvino/core/type/float16.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/parameter.hpp>
#include <openvino/runtime/tensor.hpp>
#include <ostream>
#include <set>
#include <stdexcept>
#include <string>
#include <cstring>
#include <unordered_map>
#include <vector>
GgmlOvDecoder::GgmlOvDecoder(ggml_cgraph * cgraph,
@@ -98,27 +100,119 @@ GgmlOvDecoder::GgmlOvDecoder(ggml_cgraph * cgraph, std::map<std::string, std::sh
}
}
namespace {
bool is_inplace_op(const ggml_tensor * node) {
return node->op == GGML_OP_SET_ROWS || node->op == GGML_OP_CPY || (node->op == GGML_OP_SCALE && node->view_src);
}
bool is_same_shape(const ggml_tensor * a, const ggml_tensor * b) {
for (int i = 0; i < GGML_MAX_DIMS; i++) {
if (a->ne[i] != b->ne[i]) {
return false;
}
}
return true;
}
bool is_conv_states_all_tensor(const ggml_tensor * tensor) {
return tensor != nullptr && strncmp(tensor->name, "conv_states_all", strlen("conv_states_all")) == 0;
}
// CPY writing the tail of conv_input (the concat of the previous conv state and the new tokens)
// back into a slot block of the recurrent state cache. Detected structurally because the rollback
// variant (cparams.n_rs_seq > 0) emits one such CPY per snapshot slot without naming them.
bool is_conv_state_writeback(const ggml_tensor * node) {
return node->op == GGML_OP_CPY && node->view_src != nullptr && GgmlOvDecoder::is_kvcache(node->view_src, nullptr) &&
node->src[0] != nullptr && node->src[0]->op == GGML_OP_VIEW && node->src[0]->src[0] != nullptr &&
node->src[0]->src[0]->op == GGML_OP_CONCAT && node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW &&
node->src[1]->view_src == node->view_src;
}
// MoE expert aggregation (build_moe_ffn in llama-graph.cpp): each expert plane is
// `ggml_view_2d(experts, n_embd, n_tokens, experts->nb[2], i*experts->nb[1])` and the planes
// are summed with a chain of ADDs: moe_out = ((view_0 + view_1) + view_2) + ... + view_{n-1}.
// Detected structurally by walking the ADD chain and checking every leaf is a same-shape,
// same-stride VIEW of one common base tensor, indexed by a distinct expert-plane offset, and
// that the chain covers every plane of that base (leaf count == base->ne[1]). Only the
// outermost ADD of the chain satisfies this (inner ADDs see fewer leaves than base->ne[1]).
bool is_moe_expert_sum_add(const ggml_tensor * node) {
std::vector<const ggml_tensor *> leaves;
const ggml_tensor * cur = node;
while (cur->op == GGML_OP_ADD) {
if (cur->src[0] == nullptr || cur->src[1] == nullptr) {
return false;
}
leaves.push_back(cur->src[1]);
cur = cur->src[0];
}
leaves.push_back(cur);
const ggml_tensor * base = nullptr;
std::set<int64_t> plane_indices;
for (const ggml_tensor * leaf : leaves) {
if (leaf->op != GGML_OP_VIEW || leaf->src[0] == nullptr) {
return false;
}
const ggml_tensor * leaf_base = leaf->src[0];
if (base == nullptr) {
base = leaf_base;
} else if (leaf_base != base) {
return false;
}
if (leaf->ne[0] != base->ne[0] || leaf->ne[1] != base->ne[2] || leaf->ne[2] != 1 || leaf->ne[3] != 1 ||
leaf->nb[1] != base->nb[2]) {
return false;
}
if (base->nb[1] == 0 || leaf->view_offs % base->nb[1] != 0) {
return false;
}
int64_t plane = static_cast<int64_t>(leaf->view_offs / base->nb[1]);
if (plane < 0 || plane >= base->ne[1] || !plane_indices.insert(plane).second) {
return false;
}
}
return base != nullptr && base->ne[1] > 1 && plane_indices.size() == static_cast<size_t>(base->ne[1]);
}
} // namespace
static std::string get_tensor_ov_name(const ggml_cgraph * cgraph, const ggml_tensor * tensor) {
if (tensor == nullptr) {
return "";
}
const size_t hash_pos = ggml_hash_find(&cgraph->visited_hash_set, tensor);
if (((tensor->flags & GGML_TENSOR_FLAG_COMPUTE) || GgmlOvDecoder::is_kvcache(tensor, nullptr)) &&
hash_pos != GGML_HASHSET_FULL && ggml_bitset_get(cgraph->visited_hash_set.used, hash_pos)) {
return std::string(tensor->name) + "#" + std::to_string(hash_pos);
}
return tensor->name;
}
static std::string get_tensor_graph_input_ov_name(const GgmlOvDecoder * decoder,
const ggml_cgraph * cgraph,
const ggml_tensor * tensor,
const ggml_tensor * op) {
if (GgmlOvDecoder::is_inp_pos(tensor, op)) {
return "inp_pos";
}
if (GgmlOvDecoder::is_inp_emb(tensor, op)) {
return "embd";
}
if (decoder->is_stateful() && GgmlOvDecoder::is_inp_mask(tensor, op)) {
return std::string(tensor->name).find("swa") == std::string::npos ? "self_kq_mask" : "self_kq_mask_swa";
}
return get_tensor_ov_name(cgraph, tensor);
}
void GgmlOvDecoder::set_input_output() {
for (int node_n = 0; node_n < m_cgraph->n_nodes; node_n++) {
auto node = m_cgraph->nodes[node_n];
auto * node = m_cgraph->nodes[node_n];
NodeInfo current_node_info;
auto node_name = std::string(node->name);
auto node_output_name = node_name;
auto * node_output = node;
if (node->op == GGML_OP_SET_ROWS) {
// SET_ROWS updates the tensor in place. For later ov op that uses the
// the view_src of SET_ROWS, we need to make sure they get the updated tensor
// by putting the view_src name in the tensor_map in
// <openvino>/src/frontends/ggml/src/translate_session.cpp
node_output_name = std::string(node->view_src->name);
node_output = node->view_src;
}
auto node_name = get_tensor_ov_name(m_cgraph, node);
current_node_info.node = node;
current_node_info.node_name = node_name;
current_node_info.node_output = node_output;
current_node_info.node_output_name = node_output_name;
current_node_info.node_op_case = 0;
current_node_info.data_addr = node->data;
@@ -127,9 +221,9 @@ void GgmlOvDecoder::set_input_output() {
if (src == nullptr) {
continue;
}
auto src_name = std::string(src->name);
auto src_name = get_tensor_ov_name(m_cgraph, src);
if (src->flags & GGML_TENSOR_FLAG_INPUT) {
src_name = get_graph_input_ov_name(src, node);
src_name = get_tensor_graph_input_ov_name(this, m_cgraph, src, node);
}
current_node_info.node_inputs[src_name] = src;
current_node_info.node_inputs_names.push_back(src_name);
@@ -140,9 +234,9 @@ void GgmlOvDecoder::set_input_output() {
auto current = src;
while (current != nullptr) {
auto current_name = std::string(current->name);
auto current_name = get_tensor_ov_name(m_cgraph, current);
if (current->flags & GGML_TENSOR_FLAG_INPUT) {
current_name = get_graph_input_ov_name(current, node);
current_name = get_tensor_graph_input_ov_name(this, m_cgraph, current, node);
}
view_chain.emplace_back(current_name, current);
// If current src is also a VIEW, continue traversing
@@ -166,6 +260,7 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
int op_case = 0;
switch (node->op) {
case GGML_OP_RESHAPE: {
auto name = std::string(node->name);
auto * src = node->src[0];
if (src->op == GGML_OP_RESHAPE && src->src[0]->ne[0] == node->ne[0] && src->src[0]->ne[1] == node->ne[1]) {
op_case = 4;
@@ -178,11 +273,12 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
}
} else if (src->ne[0] * src->ne[1] * src->ne[2] == node->ne[1]) {
op_case = 3;
} else if (src->ne[1] * src->ne[2] == node->ne[1]) {
op_case = 6;
}
if (op_case == 0 && ggml_nelements(node) == ggml_nelements(src)) {
} else if (name.find("linear_attn_qkv_mixed") == 0 || name.find("alpha") == 0) {
op_case = 6;
} else if (name.find("linear_attn_out") == 0) {
op_case = 7;
} else if (name.find("state_predelta") == 0) {
op_case = 8;
}
break;
}
@@ -232,7 +328,14 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
}
case GGML_OP_GET_ROWS: {
if (node->src[1]->op == GGML_OP_VIEW) {
op_case = 2;
// GET_ROWS gathering recurrent state cache rows via the inp->s_copy index list:
// src[0] is a reshape of cache_r/cache_s, src[1] is a view of the s_copy leaf.
// op_case 3: main view (active sequences, view offset 0)
// op_case 4: extra view (defrag remainder, nonzero view offset)
if (node->src[0]->op == GGML_OP_RESHAPE && node->src[0]->src[0] != nullptr &&
is_kvcache(node->src[0]->src[0], nullptr)) {
op_case = node->src[1]->view_offs == 0 ? 1 : 2;
}
}
break;
}
@@ -260,7 +363,7 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
// throw std::runtime_error("Unsupported VIEW case");
}
op_case = 0;
if (m_model_is_splitted && m_model_inputs.find(std::string(src->name)) != m_model_inputs.end()) {
if (m_model_is_splitted && m_model_inputs.find(get_tensor_ov_name(m_cgraph, src)) != m_model_inputs.end()) {
op_case = 0;
}
}
@@ -295,6 +398,56 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
}
break;
}
case GGML_OP_RMS_NORM: {
if (node->src[0]->op == GGML_OP_VIEW) {
if (is_same_shape(node->src[0]->src[0], node->src[0])) {
op_case = 1;
} else if (node->src[0]->src[0]->op == GGML_OP_GATED_DELTA_NET) {
op_case = 2;
}
}
break;
}
case GGML_OP_CPY: {
if (node->src[0]->op == GGML_OP_VIEW) {
if (node->src[0]->src[0]->op == GGML_OP_GATED_DELTA_NET) {
op_case = 1;
} else if (is_conv_state_writeback(node)) {
op_case = 2;
break;
} else if (is_conv_states_all_tensor(node->view_src) && node->src[1] != nullptr &&
node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src == node->view_src) {
op_case = 4;
break;
}
} else if (node->src[0]->op == GGML_OP_GET_ROWS && node->src[1] != nullptr &&
node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src != nullptr &&
is_kvcache(node->src[1]->view_src, nullptr)) {
// s_copy defrag remainder writeback: gathered extra state rows copied back into the cache
op_case = 3;
}
break;
}
case GGML_OP_ADD: {
if (is_moe_expert_sum_add(node)) {
// Outermost ADD of a MoE expert-plane sum chain: translated as a single
// ReduceSum over the base tensor instead of N-1 chained Adds over N Slices.
op_case = 1;
}
break;
}
case GGML_OP_SCALE: {
if (node->view_src && node->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY) {
op_case = 1;
}
break;
}
case GGML_OP_L2_NORM: {
if (std::string(node->name).find("predelta") != std::string::npos) {
op_case = 1;
}
break;
}
default:
break;
}
@@ -476,6 +629,43 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
model_params.mixed_rope_params = true;
}
}
if (node->op == GGML_OP_GATED_DELTA_NET) {
model_params.state_size = node->src[0]->ne[0];
}
if (node->op == GGML_OP_SCALE && node->view_src != nullptr && is_kvcache(node->view_src, nullptr)) {
compute_params.cache_rs_reset_len = ggml_nelements(node) / node->view_src->ne[0];
compute_params.cache_rs_reset_idx = node->src[0]->view_offs / node->view_src->ne[0];
}
// Capture the destination slot block of every recurrent state cache writeback, plus the
// conv_input window the conv state writeback copies. The active sequences occupy a
// contiguous slot block [begin, begin + n_seqs) of the cache; the block and the window move
// with the batch, so they are fed to the cached model as runtime inputs.
if (node->op == GGML_OP_CPY && node->view_src != nullptr && is_kvcache(node->view_src, nullptr) &&
node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src == node->view_src) {
const bool is_conv = is_conv_state_writeback(node);
const bool is_gdn = node->src[0]->op == GGML_OP_VIEW && node->src[0]->src[0] != nullptr &&
node->src[0]->src[0]->op == GGML_OP_GATED_DELTA_NET;
const bool is_extra = node->src[0]->op == GGML_OP_GET_ROWS;
const ggml_tensor * dest_view = node->src[1];
const ggml_tensor * cache = node->view_src;
const size_t row_bytes = cache->ne[0] * ggml_type_size(cache->type);
if (row_bytes > 0 && (is_conv || is_gdn || is_extra)) {
ComputeParams::RsWriteback writeback;
writeback.slot_begin = (int) (dest_view->view_offs / row_bytes);
if (is_conv) {
// conv_input column the copied window starts at
writeback.src_begin = (int) (node->src[0]->view_offs / node->src[0]->view_src->nb[0]);
} else if (is_gdn) {
// first row of the state part of the gated-delta-net output
writeback.src_begin = (int) (node->src[0]->view_offs / node->src[0]->view_src->nb[1]);
}
compute_params.rs_writebacks[get_tensor_ov_name(cgraph, node)] = writeback;
}
if (is_conv || is_gdn) {
compute_params.s_copy_active_slot_len = (int) dest_view->ne[1];
}
}
}
auto * output_tensor = cgraph->nodes[cgraph->n_nodes - 1];
compute_params.output_len = output_tensor->ne[1];
@@ -505,6 +695,10 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
if (is_inp_tok(input, op) || is_inp_pos(input, op)) {
// tokens or positions
int len = m_is_static ? (m_is_prefill ? m_prefill_chunk_size : 1) : -1;
if (m_is_static && is_inp_pos(input, op)) {
// IMROPE stacks n_planes (t/h/w/e) position planes back to back
len *= get_inp_pos_n_planes(op);
}
input_shape = ov::PartialShape{1, 1, 1, len};
} else if (is_output_idx(input, op)) {
@@ -543,6 +737,9 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
int len = m_is_static ? (m_is_prefill ? m_prefill_chunk_size : 1) : -1;
input_shape = ov::PartialShape{1, 1, 1, len};
} else if (is_inp_s_copy(input, op) || is_s_copy_leaf(input)) {
input_shape = ov::PartialShape{1, 1, 1, -1};
} else {
input_shape = ov::PartialShape{get_shape(input)};
}
@@ -558,6 +755,35 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
return input_shape;
}
bool GgmlOvDecoder::is_s_copy_leaf(const ggml_tensor * tensor) const {
if (tensor == nullptr || tensor->op != GGML_OP_NONE || m_cgraph == nullptr) {
return false;
}
for (int i = 0; i < m_cgraph->n_nodes; i++) {
const ggml_tensor * node = m_cgraph->nodes[i];
if (node->op != GGML_OP_GET_ROWS || node->src[0] == nullptr || node->src[1] == nullptr) {
continue;
}
// The index list may reach the s_copy leaf through one or more VIEWs.
const ggml_tensor * idx = node->src[1];
while (idx != nullptr && idx->op == GGML_OP_VIEW) {
idx = idx->src[0];
}
if (idx != tensor) {
continue;
}
// The gathered data must be a recurrent state cache (cache_r/cache_s).
const ggml_tensor * data = node->src[0];
while (data != nullptr && (data->op == GGML_OP_VIEW || data->op == GGML_OP_RESHAPE)) {
data = data->src[0];
}
if (data != nullptr && is_kvcache(data, nullptr)) {
return true;
}
}
return false;
}
void GgmlOvDecoder::add_extra_inputs() {
// Extra inputs:
// 1. `attention_size`, used in FLASH_ATTN where the shape of the matmul's are 256 aligned,
@@ -565,21 +791,7 @@ void GgmlOvDecoder::add_extra_inputs() {
// 2. `n_seq_active` and `seq_active_start`, used in FLASH_ATTN_EXT to indicate the active sequences in the batch
auto create_1d_input = [this](const std::string & name, int64_t value) {
if (m_is_static) {
auto constant =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{1}, std::vector<int64_t>{value});
constant->set_friendly_name(name);
m_model_extra_inputs[name] = constant;
} else {
auto param_node = std::make_shared<ov::op::v0::Parameter>(ov::element::i64, ov::Shape{1});
param_node->set_friendly_name(name);
param_node->output(0).get_tensor().set_names({name});
m_model_extra_inputs[name] = param_node;
auto tensor = std::make_shared<ov::Tensor>(ov::element::i64, ov::Shape{1});
*tensor->data<int64_t>() = value;
m_model_extra_input_values[name] = tensor;
}
m_model_extra_inputs[name] = {ov::element::i64, ov::Shape{1}, value, !m_is_static};
};
if (m_compute_params.attention_size != -1) {
@@ -595,6 +807,20 @@ void GgmlOvDecoder::add_extra_inputs() {
create_1d_input("token_len_per_seq", m_compute_params.token_len_per_seq);
}
// create_1d_input("token_len", m_compute_params.token_len_per_seq * m_compute_params.n_seq_active);
if (m_compute_params.cache_rs_reset_idx != -1) {
create_1d_input("cache_rs_reset_idx", m_compute_params.cache_rs_reset_idx);
create_1d_input("cache_rs_reset_len", m_compute_params.cache_rs_reset_len);
}
if (m_compute_params.s_copy_active_slot_len != -1) {
create_1d_input("s_copy_active_slot_len", m_compute_params.s_copy_active_slot_len);
}
for (const auto & [node_name, writeback] : m_compute_params.rs_writebacks) {
create_1d_input("rs_slot_begin_" + node_name, writeback.slot_begin);
create_1d_input("rs_src_begin_" + node_name, writeback.src_begin);
}
}
bool GgmlOvDecoder::node_is_used_as_src(const int node_idx) {
@@ -617,14 +843,11 @@ void GgmlOvDecoder::compute_model_inputs() {
ggml_tensor * node = m_cgraph->nodes[i];
// the node op is NONE means this node maybe as input of later nodes, we should add it to model inputs for this node.
if (node->op == GGML_OP_NONE && node_is_used_as_src(i)) {
std::string node_name(node->name);
std::string node_name = get_tensor_ov_name(m_cgraph, node);
if (m_model_weights.find(node_name) == m_model_weights.end()) {
m_inputs[node_name] = node;
auto param_node = std::make_shared<ov::op::v0::Parameter>(
get_ov_type(node), get_graph_input_shape(node, nullptr, m_node_dynamic_dims[node]));
param_node->set_friendly_name(node_name);
param_node->output(0).get_tensor().set_names({node_name});
m_model_inputs[node_name] = param_node;
m_model_inputs[node_name] = {get_ov_type(node),
get_graph_input_shape(node, nullptr, m_node_dynamic_dims[node])};
}
continue;
}
@@ -633,9 +856,9 @@ void GgmlOvDecoder::compute_model_inputs() {
if (src == nullptr) {
continue;
}
std::string src_name = std::string(src->name);
std::string src_name = get_tensor_ov_name(m_cgraph, src);
if (src->flags & GGML_TENSOR_FLAG_INPUT) {
src_name = get_graph_input_ov_name(src, node);
src_name = get_tensor_graph_input_ov_name(this, m_cgraph, src, node);
}
if (m_model_weights.find(src_name) != m_model_weights.end()) {
continue;
@@ -668,14 +891,11 @@ void GgmlOvDecoder::compute_model_inputs() {
// Resolve nested VIEW nodes by following src[0] until the first non-VIEW tensor.
while (src->op == GGML_OP_VIEW && src->src[0] != nullptr) {
src = src->src[0];
src_name = std::string(src->name);
src_name = get_tensor_ov_name(m_cgraph, src);
}
m_inputs[src_name] = src;
ov::PartialShape param_shape = get_graph_input_shape(node, src, m_node_dynamic_dims[src]);
auto param_node = std::make_shared<ov::op::v0::Parameter>(get_ov_type(src), param_shape);
param_node->set_friendly_name(src_name);
param_node->output(0).get_tensor().set_names({src_name});
m_model_inputs[src_name] = param_node;
m_model_inputs[src_name] = {get_ov_type(src),
get_graph_input_shape(node, src, m_node_dynamic_dims[src])};
}
}
}
@@ -691,8 +911,8 @@ void GgmlOvDecoder::compute_model_outputs() {
}
auto cur_node_use_count = m_cgraph->use_counts[ggml_hash_find(&m_cgraph->visited_hash_set, cur_node)];
if (cur_node_use_count == 0) {
// The output of SET_ROWS is the view_src tensor, which is updated in place. We should use the view_src name as the output name to make sure it can be correctly matched with the later ops that use the view_src.
if (cur_node != nullptr && cur_node->op == GGML_OP_SET_ROWS) {
// The output of in-place ops is the view_src tensor, which is updated in place. We should use the view_src name as the output name to make sure it can be correctly matched with the later ops that use the view_src.
if (cur_node != nullptr && ::is_inplace_op(cur_node) && ggml_nbytes(cur_node) > 0) {
cur_node = cur_node->view_src;
}
} else {
@@ -710,9 +930,9 @@ void GgmlOvDecoder::compute_model_outputs() {
}
}
if (cur_node != nullptr) {
std::string node_output_name(cur_node->name);
m_model_outputs[node_output_name] = cur_node;
m_model_output_names.push_back(node_output_name);
std::string cur_node_name = get_tensor_ov_name(m_cgraph, cur_node);
m_model_outputs[cur_node_name] = cur_node;
m_model_output_names.insert(cur_node_name);
}
}
}
@@ -740,7 +960,7 @@ const ggml_tensor * GgmlOvDecoder::get_tensor_from_name(const std::string & name
if (src == nullptr) {
break;
}
if (std::string(src->name) == name) {
if (get_tensor_ov_name(m_cgraph, src) == name) {
return src;
}
}
@@ -756,6 +976,16 @@ std::map<std::string, std::string> GgmlOvDecoder::get_kv_param_res_names() const
return kv_param_res_names;
}
// MUL_MAT_ID's src[0] is the [k, m, n_expert] expert-weight tensor. It is always a constant per-expert
// weight table -- never a computed activation -- regardless of whether the backend happened to mark its
// buffer as GGML_BACKEND_BUFFER_USAGE_WEIGHTS (test-backend-ops, for example, never sets that usage
// flag, unlike real inference). Without this, non-quantized (F16/F32/BF16) expert weights would fall
// through the check below as "not a weight", get decoded as a Parameter/activation instead of a
// Constant, and crash GatherMatmul's "only constant weights are supported" check.
static bool is_mul_mat_id_expert_weight(const ggml_tensor * node, int src_index) {
return node->op == GGML_OP_MUL_MAT_ID && src_index == 0;
}
std::map<std::string, std::shared_ptr<ov::Node>> GgmlOvDecoder::create_weight_nodes(ggml_cgraph * cgraph, bool naive) {
std::map<std::string, std::shared_ptr<ov::Node>> model_weights;
auto * nodes = cgraph->nodes;
@@ -768,13 +998,14 @@ std::map<std::string, std::shared_ptr<ov::Node>> GgmlOvDecoder::create_weight_no
continue;
}
std::string src_name(src->name);
std::string src_name = get_tensor_ov_name(cgraph, src);
if (is_rope_freqs_weight(src, node)) {
src_name = "rope_freqs.weight";
}
if (!src->view_src) {
ggml_backend_buffer * buffer = src->buffer;
if (buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS || ggml_is_quantized(src->type)) {
if (buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS || ggml_is_quantized(src->type) ||
is_mul_mat_id_expert_weight(node, i)) {
if (model_weights.find(src_name) == model_weights.end()) {
auto weight_node = create_weight_node(src, naive);
weight_node->set_friendly_name(src_name);
@@ -787,6 +1018,42 @@ std::map<std::string, std::shared_ptr<ov::Node>> GgmlOvDecoder::create_weight_no
return model_weights;
}
// Process-lifetime cache for weight nodes built from NON-OpenVINO buffers (e.g. the
// token_embd.weight copy that lives in a CPU/mmap buffer and feeds GET_ROWS). Such
// tensors have no OV buffer context to own a cached extra, so without this they are
// re-extracted/re-requantized on every (re)compile — for token_embd that is a ~1-2 GB
// F32 dequant each time. Keyed by tensor->data, which is stable for the process and
// uniquely identifies the immutable weight bytes. OV-buffer weights keep using the
// per-tensor extra cache and never reach here.
static std::mutex g_nonov_weight_cache_mutex;
static std::unordered_map<const void *, std::shared_ptr<ov::Node>> g_nonov_weight_cache;
std::set<std::string> GgmlOvDecoder::collect_weight_names(ggml_cgraph * cgraph) {
// Mirrors the name-selection logic of create_weight_nodes() but builds no nodes,
// so topology checks don't trigger weight extraction/requantization.
std::set<std::string> names;
for (int node_i = 0; node_i < cgraph->n_nodes; node_i++) {
auto * node = cgraph->nodes[node_i];
for (int i = 0; i < GGML_MAX_SRC; i++) {
auto * src = node->src[i];
if (src == nullptr) {
continue;
}
std::string src_name(src->name);
if (is_rope_freqs_weight(src, node)) {
src_name = "rope_freqs.weight";
}
if (!src->view_src) {
ggml_backend_buffer * buffer = src->buffer;
if (buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS || ggml_is_quantized(src->type)) {
names.insert(src_name);
}
}
}
}
return names;
}
std::shared_ptr<ov::Node> GgmlOvDecoder::create_weight_node(ggml_tensor * tensor, bool naive) {
const bool is_ov_buffer = ggml_backend_buffer_is_openvino(tensor->buffer);
@@ -826,6 +1093,21 @@ std::shared_ptr<ov::Node> GgmlOvDecoder::create_weight_node(ggml_tensor * tensor
return weight_node;
}
// Non-OV-buffer weights (CPU/mmap, e.g. the GET_ROWS token_embd copy) have no buffer
// context to cache an extra in, so memoize them here keyed by their (stable) data
// pointer to avoid re-extracting on every recompile. Opt-in via
// GGML_OPENVINO_REDUCE_COMPILE_MEM or GGML_OPENVINO_MEMORY_OPTIMIZE. Skip
// for `naive` (test/naive path) since use_bias changes the produced node.
const bool cacheable_nonov = ggml_openvino_reduce_compile_mem_enabled() && !is_ov_buffer &&
!naive && tensor->data != nullptr;
if (cacheable_nonov) {
std::lock_guard<std::mutex> lock(g_nonov_weight_cache_mutex);
auto it = g_nonov_weight_cache.find(tensor->data);
if (it != g_nonov_weight_cache.end()) {
return it->second;
}
}
// There are three cases where we need to create a new weight node:
// 1. weights are in openvino_host_buffer. Weight loading to host buffer will not trigger backend_buffer_set_tensor
// 2. weights are in cpu/cpu_mapped buffer. On token_embd.weight goes to case 1 or 2, depending on whether mmap or direct_io is used
@@ -834,7 +1116,7 @@ std::shared_ptr<ov::Node> GgmlOvDecoder::create_weight_node(ggml_tensor * tensor
// GGML_LOG_DEBUG("%s: creating new weight node for %s\n", __func__, tensor->name);
static const std::set<ggml_type> weight_types = {GGML_TYPE_F32, GGML_TYPE_F16, GGML_TYPE_BF16, GGML_TYPE_Q8_0,
GGML_TYPE_Q4_0, GGML_TYPE_Q4_1, GGML_TYPE_Q5_1, GGML_TYPE_Q4_K,
GGML_TYPE_Q5_K, GGML_TYPE_Q6_K};
GGML_TYPE_Q5_K, GGML_TYPE_Q6_K, GGML_TYPE_MXFP4};
if (weight_types.find(tensor->type) == weight_types.end()) {
throw std::runtime_error("Unexpected weight tensor type: " + std::string(tensor->name) + " with type " +
ggml_type_name(tensor->type));
@@ -863,6 +1145,12 @@ std::shared_ptr<ov::Node> GgmlOvDecoder::create_weight_node(ggml_tensor * tensor
ov_weight.weight_node->set_friendly_name(tensor->name);
if (!is_ov_buffer) {
if (cacheable_nonov) {
std::lock_guard<std::mutex> lock(g_nonov_weight_cache_mutex);
// Another thread may have inserted concurrently; keep the first.
auto [it, inserted] = g_nonov_weight_cache.emplace(tensor->data, ov_weight.weight_node);
return it->second;
}
return ov_weight.weight_node;
}
@@ -1178,7 +1466,7 @@ std::string GgmlOvDecoder::get_view_input_name(int node_idx, const std::string &
auto it = m_node_info_list[node_idx].node_inputs_views.find(name);
if (it != m_node_info_list[node_idx].node_inputs_views.end()) {
if (view_index < it->second.size()) {
return it->second[view_index].second->name;
return it->second[view_index].first;
}
}
return "";
@@ -1190,7 +1478,7 @@ std::string GgmlOvDecoder::get_view_input_src_name(int node_idx, const std::stri
if (view_index < it->second.size()) {
auto * view_tensor = it->second[view_index].second;
if (view_tensor && view_tensor->src[0]) {
return view_tensor->src[0]->name;
return get_tensor_ov_name(m_cgraph, view_tensor->src[0]);
}
}
}
@@ -1214,7 +1502,7 @@ std::vector<std::string> GgmlOvDecoder::get_input_names(int node_idx) const {
}
ov::PartialShape GgmlOvDecoder::get_output_shape(int node_idx) const {
auto * ggml_tensor = m_node_info_list[node_idx].node_output;
auto * ggml_tensor = m_node_info_list[node_idx].node;
return ov::PartialShape(get_shape(ggml_tensor));
}
@@ -1228,7 +1516,28 @@ std::vector<size_t> GgmlOvDecoder::get_output_stride(int node_idx) const {
}
std::vector<std::string> GgmlOvDecoder::get_output_names(int node_idx) const {
return {m_node_info_list[node_idx].node_output_name};
return {m_node_info_list[node_idx].node_name};
}
std::string GgmlOvDecoder::get_inplace_op_src(int node_idx) const {
auto * node = m_node_info_list[node_idx].node;
if (!::is_inplace_op(node) || node->view_src == nullptr || ggml_nbytes(node) == 0) {
return "";
}
const int op_case = m_node_info_list[node_idx].node_op_case;
if (node->op == GGML_OP_CPY && (op_case == 1 || op_case == 2 || op_case == 3) &&
m_compute_params.s_copy_active_slot_len == -1) {
return "";
}
return get_tensor_ov_name(m_cgraph, node->view_src);
}
bool GgmlOvDecoder::is_view_like_alias_of(int node_idx, const std::string & view_src_name) const {
auto * node = m_node_info_list[node_idx].node;
if (node->view_src == nullptr || get_tensor_ov_name(m_cgraph, node->view_src) != view_src_name) {
return false;
}
return node->op == GGML_OP_RESHAPE || node->op == GGML_OP_VIEW;
}
const std::string & GgmlOvDecoder::get_op_name() const {
@@ -1404,14 +1713,18 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
}
if (m_node_dynamic_dims[node] != -1 && dynamic_dim_value != node->ne[m_node_dynamic_dims[node]]) {
m_node_dynamic_dims[node] = -1;
// std::cout << "Warning: Dynamic dim value mismatch for node: " << node->name
// << " and its src[0]: " << node->src[0]->name << std::endl;
GGML_LOG_WARN("ggml-openvino: dynamic dim value mismatch for VIEW node '%s', src[0]: '%s'\n",
node->name, node->src[0]->name);
}
}
break;
}
case GGML_OP_TRANSPOSE:
case GGML_OP_RESHAPE: {
if (is_same_shape(node->src[0], node)) {
m_node_dynamic_dims[node] = m_node_dynamic_dims[node->src[0]];
break;
}
// RESHAPE requires src[0] to be contiguous, so both src and result
// have standard compact strides: nb[i] = type_size * prod(ne[0..i-1]).
// Match src->nb[dynamic_dim] against result->nb[i] to find the output
@@ -1429,7 +1742,7 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
}
}
if (m_node_dynamic_dims[node] == -1) {
// std::cout << "Cannot determine dynamic dim for RESHAPE node: " << node->name << std::endl;
GGML_LOG_WARN("ggml-openvino: cannot determine dynamic dim for RESHAPE node '%s'\n", node->name);
}
}
break;
@@ -1480,15 +1793,29 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
}
if (matched_dim_count != 1) {
m_node_dynamic_dims[node] = -1;
// std::cout << "Warning: Cannot determine dynamic dim for CONT node: " << node->name
// << " and its src[0]: " << node->src[0]->name << std::endl;
GGML_LOG_WARN("ggml-openvino: cannot determine dynamic dim for CONT node '%s', src[0]: '%s'\n",
node->name, node->src[0]->name);
}
}
}
break;
case GGML_OP_CONCAT:
for (int i = 0; i < GGML_MAX_DIMS; i++) {
if (node->src[0]->ne[i] != node->ne[i]) {
m_node_dynamic_dims[node] = i;
break;
}
}
break;
case GGML_OP_SSM_CONV:
case GGML_OP_GATED_DELTA_NET:
m_node_dynamic_dims[node] = 1;
break;
case GGML_OP_RMS_NORM:
case GGML_OP_L2_NORM:
case GGML_OP_NORM:
case GGML_OP_ADD:
case GGML_OP_SUB:
case GGML_OP_GLU:
case GGML_OP_ROPE:
case GGML_OP_SCALE:
@@ -1496,9 +1823,31 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
case GGML_OP_ARGSORT:
case GGML_OP_ADD_ID:
case GGML_OP_UNARY:
case GGML_OP_CUMSUM:
case GGML_OP_FILL:
case GGML_OP_SET:
case GGML_OP_DIAG:
case GGML_OP_TRI:
case GGML_OP_REPEAT:
// Shape-preserving elementwise ops: the dynamic dim is unchanged from src[0].
// DIV/CLAMP are used in the MoE routing-weight normalization
// (sum_rows -> clamp -> div). If they are left untracked here the dynamic
// (token) dim is lost there, the captured prefill token count gets baked into
// the downstream reshapes, and every decoder layer after layer 0 turns static
// (which then triggers the GPU in-place-concat KV-cache corruption).
case GGML_OP_DIV:
case GGML_OP_CLAMP:
case GGML_OP_PAD:
m_node_dynamic_dims[node] = m_node_dynamic_dims[node->src[0]];
break;
case GGML_OP_SUM_ROWS:
// SUM_ROWS reduces ggml axis 0 to size 1 and preserves all other axes, so the
// dynamic dim is preserved unless it was axis 0 (then it is summed away).
m_node_dynamic_dims[node] =
(m_node_dynamic_dims[node->src[0]] == 0) ? -1 : m_node_dynamic_dims[node->src[0]];
break;
case GGML_OP_MUL_MAT_ID:
case GGML_OP_SOLVE_TRI:
m_node_dynamic_dims[node] = m_node_dynamic_dims[node->src[1]];
break;
case GGML_OP_CPY:
@@ -1534,7 +1883,8 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
break;
}
default:
// std::cout << "Doesn't handle node name: " << node->name << " op: " << ggml_op_name(node->op) << std::endl;
GGML_LOG_DEBUG("ggml-openvino: compute_node_dynamic_dims: unhandled op %s for node '%s'\n",
ggml_op_name(node->op), node->name);
break;
}
};
+83 -15
View File
@@ -11,6 +11,8 @@
#include <memory>
#include <openvino/core/partial_shape.hpp>
#include <optional>
#include <set>
#include <string>
#include <vector>
struct ModelParams {
@@ -20,6 +22,7 @@ struct ModelParams {
int n_seq = 1;
int n_heads_kv = -1;
int head_size = -1;
int state_size = -1; // for SSM molels, eg qwen35
int32_t rope_params[15];
bool mixed_rope_params = false;
std::vector<int> swa_layers;
@@ -48,6 +51,47 @@ struct ComputeParams {
int token_len_per_seq = -1;
int past_kv_len = -1;
int output_len = 1;
int cache_rs_reset_idx = -1;
int cache_rs_reset_len = -1;
// SSM/DeltaNet models otionally clear cache_r and cache_s of certain slots in the cgraph
// 3: [ 18432, 4, 1, 1] RESHAPE cache_r_l0 (reshaped)
// [ 18432, 4, 1, 1] 0: NONE cache_r_l0
// 4: [ 18432, 1, 1, 1] VIEW cache_r_l0 (reshaped) (view)
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// 5: [ 18432, 1, 1, 1] SCALE cache_r_l0 (reshaped) (view) (view)
// [ 18432, 1, 1, 1] 0: VIEW cache_r_l0 (reshaped) (view)
int s_copy_active_slot_len = -1;
// SSM/DeltaNet models otionally reorder slots of state cache, to make the active slots contiguous
// leaf_5 is the inp->s_copy in llama-graph.cpp, eg if there are 8 slots in total and slot 3 and 7
// are active in the current batch, leaf_5 will be [3, 7, 5, 6, 4]
// 6: [ 2, 1, 1, 1] VIEW (view)
// [ 2, 1, 1, 1] 0: NONE leaf_5
// 7: [ 18432, 2, 1, 1] GET_ROWS conv_states-0
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// [ 2, 1, 1, 1] 1: VIEW (view)
// 8: [ 0, 1, 1, 1] VIEW (view)
// [ 2, 1, 1, 1] 0: NONE leaf_5
// 9: [ 18432, 0, 1, 1] GET_ROWS node_9
// [ 18432, 4, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)
// [ 0, 1, 1, 1] 1: VIEW (view)
// 10: [ 18432, 0, 1, 1] VIEW cache_r_l0 (view)
// [ 18432, 4, 1, 1] 0: NONE cache_r_l0
// 11: [ 18432, 0, 1, 1] CPY cache_r_l0 (view) (copy of )
// [ 18432, 0, 1, 1] 0: GET_ROWS node_9
// [ 18432, 0, 1, 1] 1: VIEW cache_r_l0 (view)
struct RsWriteback {
int slot_begin = 0; // first cache slot written by the CPY
int src_begin = 0; // where the copied data starts in the source tensor (in rows of it)
};
std::map<std::string, RsWriteback> rs_writebacks;
// Offsets of the state cache writeback CPY nodes, keyed by node name. They change with the
// batch (kv head, active sequence count, token count) and, with rollback enabled
// (cparams.n_rs_seq > 0), the conv state is written back once per snapshot slot, each snapshot
// taking a different conv_input window. Passed to the cached model as runtime inputs.
};
class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder {
@@ -59,8 +103,6 @@ public:
std::map<std::string, ggml_tensor *> node_inputs;
std::map<std::string, std::vector<std::pair<std::string, ggml_tensor *>>> node_inputs_views;
std::vector<std::string> node_inputs_names;
ggml_tensor * node_output;
std::string node_output_name;
int node_op_case = 0;
void * data_addr;
};
@@ -156,6 +198,10 @@ public:
virtual std::vector<std::string> get_output_names(int node_idx) const override;
virtual std::string get_inplace_op_src(int node_idx) const override;
virtual bool is_view_like_alias_of(int node_idx, const std::string & view_src_name) const override;
virtual const std::string & get_op_type() const override;
virtual const std::string & get_op_type(int node_idx) const override;
@@ -173,23 +219,19 @@ public:
virtual int get_op_case(int node_idx) const override { return m_node_info_list[node_idx].node_op_case; }
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_inputs() const override {
virtual const std::map<std::string, ov::frontend::ggml::ModelInputInfo> & get_model_inputs() const override {
return m_model_inputs;
}
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_extra_inputs() const override {
virtual const std::map<std::string, ov::frontend::ggml::ModelExtraInputInfo> & get_model_extra_inputs() const override {
return m_model_extra_inputs;
}
virtual const std::map<std::string, std::shared_ptr<ov::Tensor>> & get_model_extra_input_values() const {
return m_model_extra_input_values;
}
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_weights() const override {
return m_model_weights;
}
virtual std::vector<std::string> get_model_output_names() const override { return m_model_output_names; }
virtual std::set<std::string> get_model_output_names() const override { return m_model_output_names; }
const std::map<std::string, ggml_tensor *> & get_model_outputs() const { return m_model_outputs; }
@@ -214,6 +256,8 @@ public:
virtual bool has_mixed_rope_params() const override { return m_model_params.mixed_rope_params; }
virtual int get_ssm_state_size() const override { return m_model_params.state_size; }
virtual std::map<std::string, std::string> get_kv_param_res_names() const override;
virtual bool is_static() const override { return m_is_static; }
@@ -235,6 +279,11 @@ public:
static std::map<std::string, std::shared_ptr<ov::Node>> create_weight_nodes(ggml_cgraph * cgraph,
bool naive = false);
// Collect just the set of weight-tensor names referenced by the graph, without
// building (or requantizing) any OV weight nodes. Used by topology checks like
// is_model_splitted that only need name membership.
static std::set<std::string> collect_weight_names(ggml_cgraph * cgraph);
const ggml_tensor * get_tensor_used_op(const ggml_tensor * tensor) const;
const ggml_tensor * get_tensor_from_name(const std::string & name) const;
@@ -274,6 +323,12 @@ public:
return op->op == GGML_OP_ROPE && tensor == op->src[1];
}
// IMROPE packs 4 stacked position planes (t/h/w/e) into inp_pos, each of length
// n_tokens; other modes carry a single position per token.
inline static int get_inp_pos_n_planes(const ggml_tensor * op) {
return op->op_params[2] == GGML_ROPE_TYPE_IMROPE ? 4 : 1;
}
inline static bool is_inp_emb(const ggml_tensor * tensor, const ggml_tensor * op) {
return tensor->op == GGML_OP_GET_ROWS && op->op == GGML_OP_RMS_NORM;
}
@@ -287,8 +342,12 @@ public:
return op->op == GGML_OP_ROPE && tensor == op->src[2];
}
// also returns true for cache_s and cache_r in SSM/DeltaNet models
inline static bool is_kvcache(const ggml_tensor * tensor, const ggml_tensor * op) {
return tensor->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY ||
if (tensor == nullptr) {
return false;
}
return (tensor->buffer != nullptr && tensor->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY) ||
(op != nullptr && op->op == GGML_OP_SET_ROWS && op->src[2] == tensor);
}
@@ -301,7 +360,13 @@ public:
op->src[1]->op == GGML_OP_NONE;
}
std::string get_graph_input_ov_name(const ggml_tensor * tensor, const ggml_tensor * op) {
// the state permutation index input used in SSM/DeltaNet models (inp->s_copy in llama-graph.cpp)
inline static bool is_inp_s_copy(const ggml_tensor * tensor, const ggml_tensor * op) {
return op->op == GGML_OP_GET_ROWS && tensor == op->src[1] &&
op->src[0]->buffer->usage == GGML_BACKEND_BUFFER_USAGE_ANY;
}
std::string get_graph_input_ov_name(const ggml_tensor * tensor, const ggml_tensor * op) const {
if (is_inp_pos(tensor, op)) {
return "inp_pos";
}
@@ -321,6 +386,10 @@ private:
void compute_model_inputs();
void compute_model_outputs();
// True if tensor is the inp->s_copy index leaf gathered by a recurrent state cache GET_ROWS
// (possibly through a VIEW), so it gets a dynamic [1,1,1,-1] graph-input shape.
bool is_s_copy_leaf(const ggml_tensor * tensor) const;
// Infer and propagate dynamic-dimension indices for all tensors in the GGML graph.
void compute_node_dynamic_dims();
@@ -329,12 +398,11 @@ private:
ggml_cgraph * m_cgraph = nullptr;
std::map<std::string, ggml_tensor *> m_inputs;
std::map<std::string, std::shared_ptr<ov::Node>> m_model_inputs;
std::map<std::string, std::shared_ptr<ov::Node>> m_model_extra_inputs;
std::map<std::string, std::shared_ptr<ov::Tensor>> m_model_extra_input_values;
std::map<std::string, ov::frontend::ggml::ModelInputInfo> m_model_inputs;
std::map<std::string, ov::frontend::ggml::ModelExtraInputInfo> m_model_extra_inputs;
std::map<std::string, std::shared_ptr<ov::Node>> m_model_weights;
std::map<std::string, ggml_tensor *> m_model_outputs;
std::vector<std::string> m_model_output_names;
std::set<std::string> m_model_output_names;
std::vector<NodeInfo> m_node_info_list;
std::map<ggml_tensor *, int> m_node_dynamic_dims;
+54 -5
View File
@@ -31,6 +31,7 @@ void ggml_openvino_device_config::init() {
// String values (use ggml_openvino_getenv_str)
"GGML_OPENVINO_DEVICE",
"GGML_OPENVINO_CACHE_DIR",
"GGML_OPENVINO_DEBUG_NODE",
// Integer values (use ggml_openvino_getenv_int)
"GGML_OPENVINO_PREFILL_CHUNK_SIZE",
// Boolean toggles (treated as int flags via ggml_openvino_getenv_int)
@@ -44,7 +45,12 @@ void ggml_openvino_device_config::init() {
"GGML_OPENVINO_ENABLE_CACHE",
"GGML_OPENVINO_DISABLE_CACHE",
"GGML_OPENVINO_DISABLE_KV_SLICE",
"GGML_OPENVINO_ENABLE_FALLBACK",
"GGML_OPENVINO_MANUAL_GQA_ATTN",
"GGML_OPENVINO_MEMORY_OPTIMIZE",
"GGML_OPENVINO_RELEASE_WEIGHTS",
"GGML_OPENVINO_REDUCE_COMPILE_MEM",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR",
};
for (const char * const & env_var : env_var_names) {
@@ -168,6 +174,22 @@ int ggml_openvino_getenv_int(const char * var, int default_value) {
return v ? std::atoi(v) : default_value;
}
bool ggml_openvino_reduce_compile_mem_enabled() {
const char * reduce_compile_mem = ggml_openvino_getenv_str("GGML_OPENVINO_REDUCE_COMPILE_MEM");
if (reduce_compile_mem != nullptr) {
return ggml_openvino_getenv_int("GGML_OPENVINO_REDUCE_COMPILE_MEM") != 0;
}
return ggml_openvino_getenv_int("GGML_OPENVINO_MEMORY_OPTIMIZE") != 0;
}
bool ggml_openvino_release_weights_enabled(const std::string & device) {
const char * release_weights = ggml_openvino_getenv_str("GGML_OPENVINO_RELEASE_WEIGHTS");
if (release_weights != nullptr) {
return device == "GPU" && ggml_openvino_getenv_int("GGML_OPENVINO_RELEASE_WEIGHTS") != 0;
}
return device == "GPU" && ggml_openvino_getenv_int("GGML_OPENVINO_MEMORY_OPTIMIZE") != 0;
}
// Check if running on NPU
bool ggml_openvino_is_npu() {
return ggml_openvino_get_device_config().is_npu;
@@ -252,14 +274,31 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
return layout;
}
// Only handle 2D weight tensors
if (tensor->ne[2] != 1 || tensor->ne[3] != 1) {
// Most quantized weights use the existing 2D extraction path. 3D expert weights for
// MUL_MAT_ID (MoE) are also supported, either as MXFP4 (packed, dedicated branch below) or via the
// generic sizing math below, which is shape-agnostic (based on total element count). Only reject 4D.
if (tensor->ne[3] != 1) {
return layout;
}
// 3D MoE expert weights that are not requantized (see below) always use the exact f16
// zero-point extraction (see extract_quantized_weights), which needs a wider zp slot than
// the packed integer zero point -- must be kept in sync with that function so the buffer
// sizing here matches what process_weight_tensor actually writes.
const bool for_gather_matmul = tensor->ne[2] > 1;
int64_t n_elements = ggml_nelements(tensor);
const size_t alignment = 64; // Good for SIMD
if (tensor->type == GGML_TYPE_MXFP4 && (tensor->ne[2] > 1 || tensor->ne[3] > 1)) {
layout.weights_per_block = 32;
layout.is_symmetric = true;
layout.weights_size = ggml_nbytes(tensor);
layout.weights_offset = 0;
layout.total_size = layout.weights_size;
return layout;
}
// Check if requantization is needed (NPU-specific)
auto requant_type = ggml_openvino_get_requant_type(tensor, use_bias);
if (requant_type.has_value()) {
@@ -334,6 +373,11 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
layout.is_symmetric = false;
switch (tensor->type) {
case GGML_TYPE_MXFP4:
layout.is_u4 = true;
layout.is_symmetric = true;
break;
case GGML_TYPE_Q4_0:
layout.is_u4 = true;
layout.is_symmetric = true;
@@ -369,12 +413,17 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
// Weights: U4 = n_elements/2 bytes, U8 = n_elements bytes
layout.weights_size = layout.is_u4 ? (n_elements / 2) : n_elements;
// Scales: F16 per block
// Scales: F16 per block, except MXFP4 which stores one E8M0 byte per block.
int64_t n_blocks = n_elements / layout.weights_per_block;
layout.scales_size = n_blocks * sizeof(uint16_t); // F16 = 2 bytes
// For symmetric quantization, no zp needed (weights stored as signed)
layout.scales_size = n_blocks * (tensor->type == GGML_TYPE_MXFP4 ? sizeof(uint8_t) : sizeof(uint16_t));
// For symmetric quantization, no zp needed (weights stored as signed). Asymmetric
// for_gather_matmul (3D MoE expert) weights use an exact f16 zero point (see
// extract_quantized_weights/make_int8_weights/make_int4_weights), which needs one f16 per
// block instead of a packed u4/u8 integer zero point.
if (layout.is_symmetric) {
layout.zp_size = 0;
} else if (use_bias || for_gather_matmul) {
layout.zp_size = n_blocks * sizeof(uint16_t);
} else {
layout.zp_size = layout.is_u4 ? ((n_blocks + 1) / 2) : n_blocks;
}
@@ -96,9 +96,22 @@ const std::string & ggml_openvino_get_device_name();
const char * ggml_openvino_getenv_str(const char * var, const char * default_value = nullptr);
int ggml_openvino_getenv_int(const char * var, int default_value = 0);
// Memory optimization toggles. GGML_OPENVINO_MEMORY_OPTIMIZE is an umbrella
// switch; the fine-grained env vars still override it when explicitly set.
bool ggml_openvino_reduce_compile_mem_enabled();
bool ggml_openvino_release_weights_enabled(const std::string & device);
// Check if running on NPU
bool ggml_openvino_is_npu();
// Host weight-buffer release (GGML_OPENVINO_RELEASE_WEIGHTS, GPU only).
// register: record a host weight buffer (idempotent per data pointer).
// release: madvise(MADV_DONTNEED) all registered buffers, dropping their RSS.
// released: true once release has run (used to fail-fast on post-release recompile).
void ggml_openvino_register_weight_buffer(void * data, size_t size);
void ggml_openvino_release_weight_buffers();
bool ggml_openvino_weight_buffers_released();
// Get requantization type for a tensor type (returns nullopt if no requant needed)
std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor * tensor, bool no_requant = false);
+245 -103
View File
@@ -32,6 +32,7 @@
# endif
# include <windows.h>
#else
# include <sys/mman.h>
# include <unistd.h>
#endif
@@ -135,6 +136,81 @@ struct ggml_backend_openvino_buffer_type_context {
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<std::pair<void *, size_t>> 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<std::mutex> 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<std::mutex> lock(reg.mutex);
return reg.released;
}
void ggml_openvino_release_weight_buffers() {
auto & reg = ov_weight_registry();
std::lock_guard<std::mutex> 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<uintptr_t>(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<void *>(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;
@@ -235,10 +311,12 @@ static void ggml_backend_openvino_buffer_set_tensor(ggml_backend_buffer_t buffer
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)
// 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_2d) {
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);
@@ -274,6 +352,22 @@ static void ggml_backend_openvino_buffer_set_tensor(ggml_backend_buffer_t buffer
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);
@@ -458,8 +552,8 @@ static size_t ggml_backend_openvino_buffer_type_get_alloc_size(ggml_backend_buff
const ggml_tensor * tensor) {
GGML_UNUSED(buft);
// For quantized 2D tensors (weights), we need extra space for extracted data
if (ggml_is_quantized(tensor->type) && tensor->ne[2] == 1 && tensor->ne[3] == 1) {
// 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",
@@ -618,7 +712,13 @@ static void ggml_backend_openvino_free(ggml_backend_t backend) {
if (ctx->runtime_context) {
auto r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context);
if (--r_ctx->backend_count == 0) {
r_ctx->clear_caches();
// 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();
}
}
}
@@ -856,6 +956,32 @@ static bool checked_mul_size(size_t a, size_t b, size_t & out) {
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];
@@ -863,9 +989,10 @@ static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) {
return true;
}
// The current OpenVINO translation materializes selected expert weights with
// shape [n_tokens, n_used, rows, k]. Skip cases that would create a very
// large temporary on GPU and let the scheduler fall back instead.
// 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<size_t>(ids->ne[1]), tmp_elems) ||
!checked_mul_size(tmp_elems, static_cast<size_t>(ids->ne[0]), tmp_elems) ||
@@ -883,12 +1010,56 @@ static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) {
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");
}
static bool is_op_unsupported_case(const ggml_tensor * op) {
if (is_msa_block_mask_expansion(op)) {
return true;
}
switch (op->op) {
case GGML_OP_CONCAT: {
if (op->type == GGML_TYPE_I64) {
return true;
}
if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16 && has_view_op_input(op)) {
return true;
}
break;
}
case GGML_OP_SET: {
const auto nb1 = static_cast<size_t>(op->op_params[0]);
const auto nb2 = static_cast<size_t>(op->op_params[1]);
const auto nb3 = static_cast<size_t>(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]) {
// std::cout << "Unsupported SET op with dst nb1=" << nb1 << ", nb2=" << nb2 << ", nb3=" << 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")
// << std::endl;
return true;
}
break;
}
case GGML_OP_GET_ROWS:
@@ -896,23 +1067,24 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
if (op->ne[3] != 1) {
return true;
}
if (op->ne[0] == 256 && (op->src[0]->type == GGML_TYPE_Q4_K || op->src[0]->type == GGML_TYPE_Q5_K)) {
// ERR = 0.000000306 > 0.000000100 GET_ROWS(type=q4_K,n=256,m=5,r=4,be1=1,be2=1,v=0)
// ERR = 0.000000197 > 0.000000100 GET_ROWS(type=q5_K,n=256,m=5,r=4,be1=1,be2=1,v=0)
if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" &&
op->src[0]->type == GGML_TYPE_BF16) {
return true;
}
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 true;
}
// Keep the MoE routing weights gather on CPU for GPU runs. Splitting
// only at the later SUM/CLAMP/DIV nodes still leaves this routing path
// numerically unstable for arctic-style MoE graphs.
if (strncmp(op->name, "ffn_moe_weights", sizeof("ffn_moe_weights") - 1) == 0) {
return true;
}
break;
}
case GGML_OP_RESHAPE: {
if (strncmp(op->name, "ffn_moe_weights", sizeof("ffn_moe_weights") - 1) == 0 ||
strncmp(op->name, "ffn_norm_exps", sizeof("ffn_norm_exps") - 1) == 0) {
if (strncmp(op->name, "ffn_norm_exps", sizeof("ffn_norm_exps") - 1) == 0) {
return true;
}
break;
@@ -939,69 +1111,22 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
break;
}
case GGML_OP_DIV: {
bool requires_broadcast = false;
for (int i = 0; i < 4; i++) {
if (op->src[0]->ne[i] == op->src[1]->ne[i]) {
continue;
}
if (op->src[0]->ne[i] != 1 && op->src[1]->ne[i] != 1) {
return true;
}
requires_broadcast = true;
}
// 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 (requires_broadcast && ggml_openvino_get_device_name() == "GPU") {
return true;
}
// qwen3next MoE weight normalization is numerically sensitive on the GPU
// path. Keep the normalization divide on CPU to match the reference.
if (strncmp(op->name, "ffn_moe_weights_norm", sizeof("ffn_moe_weights_norm") - 1) == 0) {
return true;
}
break;
}
case GGML_OP_SOFT_MAX: {
if (op->src[2] != nullptr) {
// GGML_LOG_WARN("OpenVINO backend does not support SOFT_MAX with sinks\n");
return true;
}
if (strncmp(op->name, "ffn_moe_probs", sizeof("ffn_moe_probs") - 1) == 0) {
return true;
}
// GPU execution of the MoE routing weights softmax is numerically unstable
// when fused with the surrounding GET_ROWS/reshape path. Keep this softmax
// on CPU so the scheduler splits at the same boundary that restores parity.
if (op->src[0] != nullptr && op->src[0]->op == GGML_OP_RESHAPE && op->src[0]->src[0] != nullptr &&
strncmp(op->src[0]->src[0]->name, "ffn_moe_weights", sizeof("ffn_moe_weights") - 1) == 0) {
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 true;
}
break;
}
case GGML_OP_SUM_ROWS: {
if (strncmp(op->name, "ffn_moe_weights_sum", sizeof("ffn_moe_weights_sum") - 1) == 0) {
return true;
}
// if the input is PERMUTE skip
if (op->src[0]->op == GGML_OP_PERMUTE) {
return true;
}
break;
}
case GGML_OP_CLAMP: {
if (strncmp(op->name, "ffn_moe_weights_sum_clamped", sizeof("ffn_moe_weights_sum_clamped") - 1) == 0) {
return true;
}
break;
}
case GGML_OP_FLASH_ATTN_EXT: {
float scale = 1.0f;
float max_bias = 0.0f;
@@ -1048,23 +1173,29 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// GGML_LOG_WARN("OpenVINO backend does not support CPY with non-contiguous data or bf16 types\n");
return true;
}
// CPY to a quantized destination (e.g. f32 -> q4_0) is numerically unstable with OpenVINO backend.
if (ggml_is_quantized(op->type)) {
return true;
}
if (ggml_nelements(op->src[0]) != ggml_nelements(op->src[1])) {
return true;
}
// 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 true;
}
// CPY into a strided view of a larger buffer (recurrent-state snapshots) not supported
if (op->view_src && ggml_nbytes(op) != ggml_nbytes(op->view_src)) {
if (!cpy_output_view_is_supported(op)) {
return true;
}
break;
}
case GGML_OP_MUL_MAT: {
if (ggml_openvino_get_device_name() == "GPU" && op->src[1]->op == GGML_OP_SOFT_MAX &&
op->src[0]->op == GGML_OP_CONT && op->src[0]->src[0] != nullptr &&
op->src[0]->src[0]->op == GGML_OP_TRANSPOSE && op->src[0]->src[0]->src[0] != nullptr &&
op->src[0]->src[0]->src[0]->op == GGML_OP_PERMUTE) {
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 true;
}
if (op->src[0]->ne[3] != op->src[1]->ne[3] && op->src[0]->ne[3] != 1 && op->src[1]->ne[3] != 1) {
@@ -1076,12 +1207,18 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
break;
}
case GGML_OP_MUL_MAT_ID: {
if (strncmp(op->name, "ffn_moe_gate_up", sizeof("ffn_moe_gate_up") - 1) == 0 ||
strncmp(op->name, "ffn_moe_down", sizeof("ffn_moe_down") - 1) == 0) {
// 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 true;
}
if (mul_mat_id_requires_large_tmp(op)) {
if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) {
return true;
}
// 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 true;
}
break;
@@ -1094,8 +1231,10 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// GGML_LOG_WARN("OpenVINO backend does not support ROPE with mode %d\n", mode);
return true;
}
if (n_dims != 0.0f && n_dims != op->src[0]->ne[0]) {
// GGML_LOG_WARN("OpenVINO backend does not support ROPE with n_dims %d != src[0]->ne[0] %ld\n", n_dims,
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) {
// GGML_LOG_WARN("OpenVINO backend does not support ROPE with n_dims %d and src[0]->ne[0] %ld\n", n_dims,
// op->src[0]->ne[0]);
return true;
}
@@ -1128,9 +1267,15 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
}
break;
}
case GGML_OP_REPEAT: {
if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16) {
return true;
}
break;
}
case GGML_OP_GATED_DELTA_NET: {
// enable after https://github.com/openvinotoolkit/openvino/pull/35917 is included in OV release
return true;
// return true;
// if (ggml_openvino_get_device_name() == "GPU" && op->src[0]->ne[2] > 1) {
// // CVS-186471
// return true;
@@ -1142,13 +1287,8 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
if (op->src[3]->ne[0] != 1) {
return true;
}
// v_repeat > 1 (GQA): ggml uses modulo head mapping (h_q = h_v % H_k)
// but the fused op uses consecutive mapping (h_q = h_v / group_size)
if (op->src[2]->ne[1] != op->src[0]->ne[1]) {
return true;
}
// K > 1 (multiple state snapshots) not supported by fused op
if (op->src[5]->ne[1] > 1) {
if (((const int32_t *) op->op_params)[0] > 1) {
return true;
}
break;
@@ -1156,11 +1296,12 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
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;
// 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
// 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 true;
}
@@ -1177,7 +1318,8 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
static std::unordered_set<ggml_type> 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_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
@@ -1224,6 +1366,9 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
// GGML_LOG_WARN("OpenVINO backend does not support unary op %s\n", ggml_unary_op_name(ggml_get_unary_op(op)));
return false;
}
if (ggml_get_unary_op(op) == GGML_UNARY_OP_EXP && op->type == GGML_TYPE_F32) {
return false;
}
break;
}
case GGML_OP_GLU: {
@@ -1232,11 +1377,11 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
// GGML_LOG_WARN("OpenVINO backend does not support GLU op %s\n", ggml_glu_op_name(ggml_get_glu_op(op)));
return false;
}
if (has_view_op_input(op)) {
// GGML_LOG_WARN("OpenVINO backend does not support unary op %s with view input\n",
// ggml_glu_op_name(ggml_get_glu_op(op)));
return false;
}
// if (has_view_op_input(op)) {
// // GGML_LOG_WARN("OpenVINO backend does not support unary op %s with view input\n",
// // ggml_glu_op_name(ggml_get_glu_op(op)));
// return false;
// }
if (op->src[1] == nullptr && op->src[0]->ne[0] % 2 != 0) {
// triggers bug in ov gpu
return false;
@@ -1249,16 +1394,11 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
// GGML_LOG_WARN("OpenVINO backend does not support op %s\n", ggml_op_name(op->op));
return false;
}
static std::set<ggml_op> ops_not_support_view_input{
GGML_OP_L2_NORM,
};
static std::set<ggml_op> 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)) {
// GGML_LOG_WARN("OpenVINO backend does not support op %s with view input\n", ggml_op_name(op->op));
return false;
}
if (op->op == GGML_OP_RMS_NORM && has_non_contiguous_view_input(op)) {
return false;
}
}
}
@@ -1275,7 +1415,9 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
// GGML_LOG_WARN("OpenVINO backend does not support tensor type %s\n", ggml_type_name(src->type));
return false;
}
if (ggml_is_quantized(src->type) && src->ne[2] != 1) {
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) {
// GGML_LOG_WARN("OpenVINO backend does not support 3D quantized tensors\n");
return false;
}
+316 -65
View File
@@ -2,6 +2,7 @@
#include "ggml-common.h"
#include "ggml-impl.h"
#include "ggml-openvino-extra.h"
#include "ggml.h"
#include <algorithm>
@@ -19,6 +20,8 @@
#include <openvino/core/type/element_type.hpp>
#include <openvino/core/type/element_type_traits.hpp>
#include <openvino/core/type/float16.hpp>
#include <openvino/core/type/float4_e2m1.hpp>
#include <openvino/core/type/float8_e8m0.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
@@ -26,6 +29,7 @@
#include <openvino/op/reshape.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/util/attr_types.hpp>
#include <openvino/pass/constant_folding.hpp>
#include <openvino/runtime/tensor.hpp>
#include <string>
#include <vector>
@@ -44,6 +48,38 @@ void unpack_32_4(const uint8_t * data, uint8_t * dst) {
}
}
static constexpr size_t MXFP4_BLOCK_SIZE = 32;
static constexpr size_t MXFP4_BLOCK_QS_SIZE = MXFP4_BLOCK_SIZE / 2;
static constexpr size_t MXFP4_BLOCK_BYTES = sizeof(uint8_t) + MXFP4_BLOCK_QS_SIZE;
static void pack_32_mxfp4_for_openvino(const uint8_t * data, uint8_t * dst) {
for (int j = 0; j < static_cast<int>(MXFP4_BLOCK_QS_SIZE); j += 2) {
const uint8_t v0 = data[j] & 0x0F;
const uint8_t v1 = (data[j + 1] & 0x0F) << 4;
const uint8_t v16 = data[j] >> 4;
const uint8_t v17 = data[j + 1] & 0xF0;
dst[j / 2] = v0 | v1;
dst[MXFP4_BLOCK_SIZE / 4 + j / 2] = v16 | v17;
}
}
void extract_mxfp4_data(const ggml_tensor * tensor, ov::Tensor & weights_arr, ov::Tensor & scales_arr) {
GGML_ASSERT(tensor->type == GGML_TYPE_MXFP4);
GGML_ASSERT(weights_arr.get_element_type() == ov::element::f4e2m1);
GGML_ASSERT(scales_arr.get_element_type() == ov::element::f8e8m0);
const auto * data = static_cast<const uint8_t *>(tensor->data);
auto * weights = static_cast<uint8_t *>(weights_arr.data());
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f8e8m0>::value_type>();
const size_t n_blocks = scales_arr.get_size();
ov::parallel_for(n_blocks, [&](size_t i) {
const uint8_t * block = data + i * MXFP4_BLOCK_BYTES;
pack_32_mxfp4_for_openvino(block + sizeof(uint8_t), weights + i * MXFP4_BLOCK_QS_SIZE);
scales[i] = ov::float8_e8m0::from_bits(block[0]);
});
}
// Extracts (weight, scales, zp) from Q4_0 tensors.
// Data layout is: |16 bit scale|32 x 4bit weights|.
// When zp_arr is empty (symmetric), weights are stored as signed i4 (value - 8).
@@ -470,22 +506,34 @@ void extract_q5_k_data(const ggml_tensor * tensor,
// TODO Reorder for make_intX_weights
// If for_gather_matmul is true, weight may be N-D (e.g. 3D MoE expert weights [n_expert, rows, cols]).
// The dequantization chain below is built as usual but left in f16 (no final Convert to f32) --
// ov::pass::MarkDequantization (registered in translate_session.cpp) marks the chain so it survives
// model-build-time ConstantFolding. mul_mat_id.cpp constructs ov::op::internal::GatherMatmul directly
// on top of the resulting f16 chain.
ov::Output<ov::Node> make_int8_weights(ov::Tensor & weight,
ov::Tensor & scales,
ov::Tensor & zp,
size_t group_size,
bool use_bias) {
bool use_bias,
bool for_gather_matmul) {
ov::Shape orig_shape = weight.get_shape();
bool is_signed = (weight.get_element_type() == ov::element::i8); // Symmetric: signed weights, no ZP
// Expand dimensions for scales and zp/bias
auto scale_shape = scales.get_shape();
ov::Shape packed_shape = {orig_shape[0], orig_shape[1] / group_size, group_size};
// Group the innermost (last) dimension. For 2D weights [rows, cols] this yields
// [rows, cols/group_size, group_size]; for 3D MoE experts [n_expert, rows, cols] this yields
// [n_expert, rows, cols/group_size, group_size].
ov::Shape packed_shape = orig_shape;
packed_shape.back() /= group_size;
packed_shape.push_back(group_size);
const size_t group_dim = packed_shape.size() - 2;
if (packed_shape[1] == 1) {
if (packed_shape[group_dim] == 1) {
// Requantized channel-wise case
packed_shape.erase(packed_shape.begin() + 1);
packed_shape.erase(packed_shape.begin() + group_dim);
} else {
scale_shape.push_back(1);
scales.set_shape(scale_shape);
@@ -505,7 +553,8 @@ ov::Output<ov::Node> make_int8_weights(ov::Tensor & weight,
static_cast<uint8_t *>(weight.data()), nullptr);
weights_node->get_rt_info()["__gguf_tensor_holder"] = weight;
auto weights_f16 = std::make_shared<ov::op::v0::Convert>(weights_node, ov::element::f16);
result = std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
auto mul = std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = mul;
} else {
// Unsigned path
auto weights_node = std::make_shared<ov::op::v0::Constant>(ov::element::u8, packed_shape,
@@ -514,11 +563,25 @@ ov::Output<ov::Node> make_int8_weights(ov::Tensor & weight,
auto weights_f16 = std::make_shared<ov::op::v0::Convert>(weights_node, ov::element::f16);
if (use_bias && zp.get_size() > 0) {
// Bias path: w * s + b (zp tensor holds f16 bias values)
auto bias_f16 = std::make_shared<ov::op::v0::Constant>(zp);
auto w_s =
std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Add>(w_s, bias_f16, ov::op::AutoBroadcastType::NUMPY);
// Accurate dequant in the FUSABLE zero-point form: (w - zp) * s, where the zero
// point is an exact f16 value zp = -bias/scale (the zp tensor holds bias values
// coming in). Algebraically equal to w*s + bias, but unlike an Add(bias) graph this
// matches CompressedWeightsBlock's pattern (Constant->Convert->Subtract->Multiply),
// so for_gather_matmul weights still fuse into GatherMatmulCompressed. Also avoids
// the round(min/scale) error of an integer zero point. Convert bias -> zero-point IN
// PLACE in the (possibly buffer-backed) zp tensor to avoid a duplicate allocation.
auto * bias_zp_data = zp.data<ov::float16>();
const auto * scale_data = scales.data<ov::float16>();
const size_t n = zp.get_size();
for (size_t i = 0; i < n; i++) {
float s = static_cast<float>(scale_data[i]);
float b = static_cast<float>(bias_zp_data[i]);
bias_zp_data[i] = ov::float16(s != 0.0f ? -b / s : 0.0f);
}
auto zero_point_f16 = std::make_shared<ov::op::v0::Constant>(zp);
auto w_zp =
std::make_shared<ov::op::v1::Subtract>(weights_f16, zero_point_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
} else {
// Zero point path: (w - zp) * s
auto zero_point = std::make_shared<ov::op::v0::Constant>(zp);
@@ -529,37 +592,49 @@ ov::Output<ov::Node> make_int8_weights(ov::Tensor & weight,
auto zero_point_f16 = std::make_shared<ov::op::v0::Convert>(zero_point, ov::element::f16);
auto w_zp =
std::make_shared<ov::op::v1::Subtract>(weights_f16, zero_point_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
auto mul = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = mul;
}
}
if (packed_shape.size() != 2) {
if (packed_shape.size() != orig_shape.size()) {
// If not requantized channel-wise case, reshape back to original shape
auto final_shape =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{orig_shape.size()}, orig_shape);
result = std::make_shared<ov::op::v1::Reshape>(result, final_shape, false);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(result, final_shape, false);
result = reshaped;
}
if (for_gather_matmul) {
return result;
}
return std::make_shared<ov::op::v0::Convert>(result, ov::element::f32);
}
// See make_int8_weights for the meaning of for_gather_matmul.
ov::Output<ov::Node> make_int4_weights(ov::Tensor & weight,
ov::Tensor & scales,
ov::Tensor & zp,
size_t group_size,
bool use_bias) {
bool use_bias,
bool for_gather_matmul) {
ov::Shape orig_weight_shape = weight.get_shape();
bool is_signed = (weight.get_element_type() == ov::element::i4); // Symmetric: signed weights, no ZP
// Expand dimensions for scales and zp/bias
ov::Shape scale_shape = scales.get_shape();
// Create INT4 weight tensor
ov::Shape packed_shape = {orig_weight_shape[0], orig_weight_shape[1] / group_size, group_size};
// Create INT4 weight tensor. Group the innermost (last) dimension: for 2D weights
// [rows, cols] this yields [rows, cols/group_size, group_size]; for 3D MoE experts
// [n_expert, rows, cols] this yields [n_expert, rows, cols/group_size, group_size].
ov::Shape packed_shape = orig_weight_shape;
packed_shape.back() /= group_size;
packed_shape.push_back(group_size);
const size_t group_dim = packed_shape.size() - 2;
if (packed_shape[1] == 1) {
if (packed_shape[group_dim] == 1) {
// Requantized channel-wise case
packed_shape.erase(packed_shape.begin() + 1);
packed_shape.erase(packed_shape.begin() + group_dim);
} else {
scale_shape.push_back(1);
scales.set_shape(scale_shape);
@@ -579,7 +654,8 @@ ov::Output<ov::Node> make_int4_weights(ov::Tensor & weight,
static_cast<uint8_t *>(weight.data()), nullptr);
weights_node->get_rt_info()["__gguf_tensor_holder"] = weight;
auto weights_f16 = std::make_shared<ov::op::v0::Convert>(weights_node, ov::element::f16);
result = std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
auto mul = std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = mul;
} else {
// Unsigned path
auto weights_node = std::make_shared<ov::op::v0::Constant>(ov::element::u4, packed_shape,
@@ -588,11 +664,23 @@ ov::Output<ov::Node> make_int4_weights(ov::Tensor & weight,
auto weights_f16 = std::make_shared<ov::op::v0::Convert>(weights_node, ov::element::f16);
if (use_bias && zp.get_size() > 0) {
// Bias path: w * s + b (zp tensor holds f16 bias values)
auto bias_f16 = std::make_shared<ov::op::v0::Constant>(zp);
auto w_s =
std::make_shared<ov::op::v1::Multiply>(weights_f16, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Add>(w_s, bias_f16, ov::op::AutoBroadcastType::NUMPY);
// Accurate dequant in the FUSABLE zero-point form: (w - zp) * s with an exact f16
// zp = -bias/scale. Equivalent to w*s + bias but matches CompressedWeightsBlock's
// pattern so for_gather_matmul weights still fuse into GatherMatmulCompressed, and
// avoids the round(min/scale) error of an integer zp. Convert bias -> zero-point IN
// PLACE in the (possibly buffer-backed) zp tensor to avoid a duplicate allocation.
auto * bias_zp_data = zp.data<ov::float16>();
const auto * scale_data = scales.data<ov::float16>();
const size_t n = zp.get_size();
for (size_t i = 0; i < n; i++) {
float s = static_cast<float>(scale_data[i]);
float b = static_cast<float>(bias_zp_data[i]);
bias_zp_data[i] = ov::float16(s != 0.0f ? -b / s : 0.0f);
}
auto zero_points_f16 = std::make_shared<ov::op::v0::Constant>(zp);
auto w_zp =
std::make_shared<ov::op::v1::Subtract>(weights_f16, zero_points_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
} else {
// Zero point path: (w - zp) * s
auto zero_points_node = std::make_shared<ov::op::v0::Constant>(zp);
@@ -603,20 +691,61 @@ ov::Output<ov::Node> make_int4_weights(ov::Tensor & weight,
auto zero_points_f16 = std::make_shared<ov::op::v0::Convert>(zero_points_node, ov::element::f16);
auto w_zp =
std::make_shared<ov::op::v1::Subtract>(weights_f16, zero_points_f16, ov::op::AutoBroadcastType::NUMPY);
result = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
auto mul = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY);
result = mul;
}
}
if (packed_shape.size() != 2) {
if (packed_shape.size() != orig_weight_shape.size()) {
// If not requantized channel-wise case, reshape back to original shape
auto final_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{orig_weight_shape.size()},
orig_weight_shape);
result = std::make_shared<ov::op::v1::Reshape>(result, final_shape, false);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(result, final_shape, false);
result = reshaped;
}
if (for_gather_matmul) {
return result;
}
return std::make_shared<ov::op::v0::Convert>(result, ov::element::f32);
}
ov::Output<ov::Node> make_mxfp4_weights(ov::Tensor & weight, ov::Tensor & scales) {
const ov::Shape final_shape = weight.get_shape();
GGML_ASSERT(!final_shape.empty());
GGML_ASSERT(final_shape.back() % MXFP4_BLOCK_SIZE == 0);
ov::Shape packed_shape = final_shape;
packed_shape.back() /= MXFP4_BLOCK_SIZE;
packed_shape.push_back(MXFP4_BLOCK_SIZE);
ov::Shape scale_shape = packed_shape;
scale_shape.back() = 1;
scales.set_shape(scale_shape);
auto weights_node = std::make_shared<ov::op::v0::Constant>(ov::element::f4e2m1, packed_shape,
static_cast<uint8_t *>(weight.data()), nullptr);
weights_node->get_rt_info()["__gguf_tensor_holder"] = weight;
auto weights_f32 = std::make_shared<ov::op::v0::Convert>(weights_node, ov::element::f32);
auto scales_node = std::make_shared<ov::op::v0::Constant>(scales);
auto scales_f32 = std::make_shared<ov::op::v0::Convert>(scales_node, ov::element::f32);
ov::Output<ov::Node> result =
std::make_shared<ov::op::v1::Multiply>(weights_f32, scales_f32, ov::op::AutoBroadcastType::NUMPY);
auto final_shape_node =
std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{final_shape.size()}, final_shape);
return std::make_shared<ov::op::v1::Reshape>(result, final_shape_node, false);
}
ov::Output<ov::Node> make_mxfp4_moe_packed_weights(ov::Tensor & weight) {
auto weights_node = std::make_shared<ov::op::v0::Constant>(ov::element::u8, weight.get_shape(),
static_cast<uint8_t *>(weight.data()), nullptr);
weights_node->get_rt_info()["__gguf_tensor_holder"] = weight;
weights_node->get_rt_info()["__ggml_openvino_mxfp4_moe_packed"] = true;
return weights_node;
}
// Extract quantized weights from tensor and create weight subgraph
std::shared_ptr<ov::Node> extract_quantized_weights(const ggml_tensor * tensor,
const void * data,
@@ -628,6 +757,13 @@ std::shared_ptr<ov::Node> extract_quantized_weights(const ggml_tensor * tensor,
ggml_tensor temp_tensor = *tensor;
temp_tensor.data = const_cast<void *>(data);
if (tensor->type == GGML_TYPE_MXFP4) {
extract_mxfp4_data(&temp_tensor, weights, scales);
auto result = make_mxfp4_weights(weights, scales).get_node_shared_ptr();
result->set_friendly_name(tensor->name);
return result;
}
// Determine block size based on tensor type
int64_t weights_per_block;
bool is_u4;
@@ -653,6 +789,13 @@ std::shared_ptr<ov::Node> extract_quantized_weights(const ggml_tensor * tensor,
std::string(ggml_type_name(tensor->type)));
}
// 3D MoE expert weights (for_gather_matmul) always use the exact f16 zero-point extraction
// (see make_int8_weights/make_int4_weights) rather than the rounded integer zero point --
// round(min/scale) error is what corrupts Q4_K/Q5_1 experts, and the f16-zp form still fuses
// into GatherMatmulCompressed since it stays a Subtract, not an Add.
const bool for_gather_matmul = tensor->ne[2] > 1;
use_bias = use_bias || for_gather_matmul;
// Extract quantized data
switch (tensor->type) {
case GGML_TYPE_Q4_0:
@@ -680,12 +823,13 @@ std::shared_ptr<ov::Node> extract_quantized_weights(const ggml_tensor * tensor,
throw std::runtime_error("Unsupported quantized type: " + std::string(ggml_type_name(tensor->type)));
}
// Create the OpenVINO weight subgraph
// Create the OpenVINO weight subgraph. 3D expert weights (MoE) are routed through the
// GatherMatmul-oriented path: dequantized in f16, with constant folding disabled on the chain.
ov::Output<ov::Node> weight_node;
if (is_u4) {
weight_node = make_int4_weights(weights, scales, zp, weights_per_block, use_bias);
weight_node = make_int4_weights(weights, scales, zp, weights_per_block, use_bias, for_gather_matmul);
} else {
weight_node = make_int8_weights(weights, scales, zp, weights_per_block, use_bias);
weight_node = make_int8_weights(weights, scales, zp, weights_per_block, use_bias, for_gather_matmul);
}
auto result = weight_node.get_node_shared_ptr();
@@ -702,28 +846,76 @@ std::shared_ptr<ov::Node> requantize_to_buffers(const ggml_tensor * tensor,
ov::Tensor & scales,
ov::Tensor & zp) {
int64_t n_elements = ggml_nelements(tensor);
const int64_t ne0 = tensor->ne[0]; // elements per row
const int64_t n_rows = n_elements / ne0;
const auto * type_traits = ggml_get_type_traits(tensor->type);
const size_t src_row_bytes = ggml_row_size(tensor->type, ne0);
// First dequantize to F32
std::vector<float> weights_f32(n_elements);
ggml_get_type_traits(tensor->type)->to_float(data, weights_f32.data(), n_elements);
// Handle F16 case - just convert and create constant
if (requant_type == ExtraQuantType::F16) {
ggml_get_type_traits(GGML_TYPE_F16)->from_float_ref(weights_f32.data(), weights.data(), n_elements);
auto result = std::make_shared<ov::op::v0::Constant>(weights);
result->set_friendly_name(tensor->name);
return result;
}
// Requantize to target quantized format
bool is_u4 = (requant_type == ExtraQuantType::Q4_0_C || requant_type == ExtraQuantType::Q4_0_128);
if (is_u4) {
quantize_q4_0(weights_f32.data(), weights, scales, zp, n_elements, block_size);
} else if (requant_type == ExtraQuantType::Q8_1_C) {
quantize_q8_1(weights_f32.data(), weights, scales, zp, n_elements, block_size);
// Streaming dequant (opt-in via GGML_OPENVINO_REDUCE_COMPILE_MEM or
// GGML_OPENVINO_MEMORY_OPTIMIZE): instead of
// materializing the full n_elements F32 array (e.g. ~1 GB for token_embd), dequantize
// a chunk of complete rows into a small scratch and quantize/convert it straight into
// the output buffers, capping the transient F32 footprint at CHUNK_ROWS*ne0 floats.
//
// Only valid (and only used) for the Q8_0_C / Q8_1_C / F16 targets whose block size
// divides a row (channel-wise _C uses block_size == ne0) so no target block straddles
// a row boundary, and Q8/F16 have no cross-block packing. The u4 (Q4_0) path packs two
// weights per byte with running zp ORs that assume a single whole-array call, so it is
// never streamed. When the flag is off, behavior is identical to the original
// full-materialization path.
const bool stream_requant = ggml_openvino_reduce_compile_mem_enabled() && !is_u4 &&
!(block_size > 0 && ne0 % block_size != 0);
if (!stream_requant) {
// Full materialization (original behavior): dequantize the whole tensor to F32,
// then convert/quantize in one call.
std::vector<float> weights_f32(n_elements);
type_traits->to_float(data, weights_f32.data(), n_elements);
if (requant_type == ExtraQuantType::F16) {
ggml_get_type_traits(GGML_TYPE_F16)->from_float_ref(weights_f32.data(), weights.data(), n_elements);
auto result = std::make_shared<ov::op::v0::Constant>(weights);
result->set_friendly_name(tensor->name);
return result;
}
if (is_u4) {
quantize_q4_0(weights_f32.data(), weights, scales, zp, n_elements, block_size);
} else if (requant_type == ExtraQuantType::Q8_1_C) {
quantize_q8_1(weights_f32.data(), weights, scales, zp, n_elements, block_size);
} else {
quantize_q8_0(weights_f32.data(), weights, scales, zp, n_elements, block_size);
}
} else {
quantize_q8_0(weights_f32.data(), weights, scales, zp, n_elements, block_size);
// Streaming path for Q8_0_C / Q8_1_C / F16 (covers token_embd, output.weight,
// and per-layer Q6_K/Q5_K requant — the large transient cases).
const int64_t CHUNK_ROWS = std::min<int64_t>(n_rows, 256);
std::vector<float> scratch(CHUNK_ROWS * ne0);
// F16 destination: 2 bytes/element, advanced per chunk by r0*ne0 elements.
auto * f16_base = static_cast<uint8_t *>(weights.data());
for (int64_t r0 = 0; r0 < n_rows; r0 += CHUNK_ROWS) {
const int64_t rows = std::min(CHUNK_ROWS, n_rows - r0);
const int64_t elems = rows * ne0;
const auto * src = static_cast<const uint8_t *>(data) + r0 * src_row_bytes;
type_traits->to_float(src, scratch.data(), elems);
if (requant_type == ExtraQuantType::F16) {
ggml_get_type_traits(GGML_TYPE_F16)
->from_float_ref(scratch.data(), f16_base + (r0 * ne0) * sizeof(uint16_t), elems);
} else {
const int64_t block_offset = (r0 * ne0) / block_size;
if (requant_type == ExtraQuantType::Q8_1_C) {
quantize_q8_1(scratch.data(), weights, scales, zp, elems, block_size, block_offset);
} else {
quantize_q8_0(scratch.data(), weights, scales, zp, elems, block_size, block_offset);
}
}
}
if (requant_type == ExtraQuantType::F16) {
auto result = std::make_shared<ov::op::v0::Constant>(weights);
result->set_friendly_name(tensor->name);
return result;
}
}
// Create the OpenVINO weight subgraph
@@ -745,8 +937,11 @@ OvWeight process_weight_tensor(const ggml_tensor * tensor, const void * data, vo
OvWeight result;
// Get 2D shape for weights [rows, cols]
ov::Shape node_shape = {static_cast<size_t>(tensor->ne[1]), static_cast<size_t>(tensor->ne[0])};
// Get shape for weights: [rows, cols], or [n_expert, rows, cols] for 3D MoE expert weights.
ov::Shape node_shape = (tensor->ne[2] > 1) ?
ov::Shape{static_cast<size_t>(tensor->ne[2]), static_cast<size_t>(tensor->ne[1]),
static_cast<size_t>(tensor->ne[0])} :
ov::Shape{static_cast<size_t>(tensor->ne[1]), static_cast<size_t>(tensor->ne[0])};
// Handle F16/F32/BF16 weights
if (tensor->type == GGML_TYPE_F32 || tensor->type == GGML_TYPE_F16 || tensor->type == GGML_TYPE_BF16) {
@@ -788,6 +983,35 @@ OvWeight process_weight_tensor(const ggml_tensor * tensor, const void * data, vo
OPENVINO_THROW("Unsupported quantized type: ", ggml_type_name(tensor->type));
}
// 3D MoE expert weights (for_gather_matmul) always use the exact f16 zero-point path (see
// extract_quantized_weights) -- must be kept in sync with the "use_bias || for_gather_matmul"
// check in ggml_openvino_get_extracted_layout, which sizes/offsets the zp slot accordingly.
// Requantized tensors (layout.is_requant) are handled by requantize_to_buffers instead, whose
// zp sizing/type is unaffected by for_gather_matmul, so they are excluded here.
const bool for_gather_matmul = tensor->ne[2] > 1;
const bool zp_is_f16 = !layout.is_requant && (use_bias || for_gather_matmul);
const bool is_3d_mxfp4_moe = tensor->type == GGML_TYPE_MXFP4 && (tensor->ne[2] > 1 || tensor->ne[3] > 1);
if (is_3d_mxfp4_moe) {
ov::Shape packed_shape = {static_cast<size_t>(tensor->ne[3]),
static_cast<size_t>(tensor->ne[2]),
static_cast<size_t>(tensor->ne[1]),
static_cast<size_t>(tensor->ne[0] / MXFP4_BLOCK_SIZE),
MXFP4_BLOCK_BYTES};
const size_t tensor_bytes = ggml_nbytes(tensor);
if (output_base_ptr) {
auto * buf_base = static_cast<uint8_t *>(output_base_ptr);
memcpy(buf_base + layout.weights_offset, data, tensor_bytes);
result.weights = ov::Tensor(ov::element::u8, packed_shape, buf_base + layout.weights_offset);
} else {
result.weights = ov::Tensor(ov::element::u8, packed_shape);
memcpy(result.weights.data(), data, tensor_bytes);
}
result.weight_node = make_mxfp4_moe_packed_weights(result.weights).get_node_shared_ptr();
result.weight_node->set_friendly_name(tensor->name);
return result;
}
if (use_bias) {
OPENVINO_ASSERT(!layout.is_requant,
"use_bias is only used for test-backend-ops, which should not have requantization");
@@ -812,24 +1036,44 @@ OvWeight process_weight_tensor(const ggml_tensor * tensor, const void * data, vo
// Quantized path (normal extraction or quantized requant)
// Create weight/scale/zp tensors - shared between both paths
// For symmetric quantization, use signed types (i4/i8) and no ZP tensor
ov::element::Type weight_type = layout.is_symmetric ? (layout.is_u4 ? ov::element::i4 : ov::element::i8) :
(layout.is_u4 ? ov::element::u4 : ov::element::u8);
ov::Shape scale_shape = {node_shape[0], node_shape[1] / layout.weights_per_block};
ov::element::Type weight_type = tensor->type == GGML_TYPE_MXFP4 ?
ov::element::f4e2m1 :
(layout.is_symmetric ? (layout.is_u4 ? ov::element::i4 : ov::element::i8) :
(layout.is_u4 ? ov::element::u4 : ov::element::u8));
ov::Shape scale_shape = node_shape;
scale_shape.back() /= layout.weights_per_block;
if (tensor->type == GGML_TYPE_MXFP4) {
if (tensor->ne[2] == 1 && tensor->ne[3] == 1) {
node_shape = {static_cast<size_t>(tensor->ne[1]), static_cast<size_t>(tensor->ne[0])};
} else {
node_shape.clear();
for (int i = GGML_MAX_DIMS - 1; i >= 0; --i) {
node_shape.push_back(static_cast<size_t>(tensor->ne[i]));
}
}
scale_shape = node_shape;
scale_shape.back() /= layout.weights_per_block;
}
if (output_base_ptr) {
uint8_t * buf_base = static_cast<uint8_t *>(output_base_ptr);
result.weights = ov::Tensor(weight_type, node_shape, buf_base + layout.weights_offset);
result.scales = ov::Tensor(ov::element::f16, scale_shape, buf_base + layout.scales_offset);
const ov::element::Type scale_type = tensor->type == GGML_TYPE_MXFP4 ? ov::element::f8e8m0 : ov::element::f16;
result.scales = ov::Tensor(scale_type, scale_shape, buf_base + layout.scales_offset);
if (!layout.is_symmetric) {
ov::element::Type zp_type = layout.is_u4 ? ov::element::u4 : ov::element::u8;
ov::element::Type zp_type =
zp_is_f16 ? ov::element::f16 : (layout.is_u4 ? ov::element::u4 : ov::element::u8);
result.zp = ov::Tensor(zp_type, scale_shape, buf_base + layout.zp_offset);
}
// else: result.zp remains default-constructed (empty) for symmetric
} else {
result.weights = ov::Tensor(weight_type, node_shape);
result.scales = ov::Tensor(ov::element::f16, scale_shape);
const ov::element::Type scale_type = tensor->type == GGML_TYPE_MXFP4 ? ov::element::f8e8m0 : ov::element::f16;
result.scales = ov::Tensor(scale_type, scale_shape);
if (!layout.is_symmetric) {
if (use_bias) {
if (zp_is_f16) {
result.zp = ov::Tensor(ov::element::f16, scale_shape);
} else {
ov::element::Type zp_type = layout.is_u4 ? ov::element::u4 : ov::element::u8;
@@ -939,16 +1183,21 @@ void quantize_q8_0(const float * x,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk) {
int64_t qk,
int64_t block_offset) {
assert(k % qk == 0);
const int nb = k / qk;
auto * weights = static_cast<uint8_t *>(weights_arr.data());
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>();
// block_offset lets a caller quantize a chunk of blocks into the right place in the
// output buffers (used for streaming requant). x points at this chunk's first block;
// outputs are advanced by block_offset blocks. Q8 has one scale/zp per block (no
// nibble packing), so any block boundary is safe.
auto * weights = static_cast<uint8_t *>(weights_arr.data()) + block_offset * qk;
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>() + block_offset;
bool is_symmetric = (weights_arr.get_element_type() == ov::element::i8); // Signed i8 path
if (!is_symmetric) {
auto * zp = static_cast<uint8_t *>(zp_arr.data());
auto * zp = static_cast<uint8_t *>(zp_arr.data()) + block_offset;
for (int i = 0; i < nb; i++) {
float amax = 0.0f;
for (int j = 0; j < qk; j++) {
@@ -990,13 +1239,15 @@ void quantize_q8_1(const float * x,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk) {
int64_t qk,
int64_t block_offset) {
assert(k % qk == 0);
const int nb = k / qk;
auto * weights = static_cast<uint8_t *>(weights_arr.data());
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>();
auto * zp = static_cast<uint8_t *>(zp_arr.data());
// See quantize_q8_0: block_offset places this chunk's output at the right block.
auto * weights = static_cast<uint8_t *>(weights_arr.data()) + block_offset * qk;
auto * scales = scales_arr.data<ov::element_type_traits<ov::element::f16>::value_type>() + block_offset;
auto * zp = static_cast<uint8_t *>(zp_arr.data()) + block_offset;
for (int i = 0; i < nb; i++) {
float min = std::numeric_limits<float>::max();
float max = std::numeric_limits<float>::lowest();
+33 -6
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <openvino/op/constant.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/runtime/tensor.hpp>
void unpack_32_4(const uint8_t * data, uint8_t * dst);
@@ -49,19 +50,38 @@ void extract_q6_k_data(const ggml_tensor * tensor,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr);
void extract_mxfp4_data(const ggml_tensor * tensor, ov::Tensor & weights_arr, ov::Tensor & scales_arr);
static constexpr size_t GGML_QUANTIZATION_GROUP_SIZE = 32;
// If for_gather_matmul is true, the weight tensor may be N-D (e.g. 3D MoE expert weights
// [n_expert, rows, cols]). The dequantization chain (Convert->[Subtract]->Multiply) is built as
// usual but left in f16 (no final Convert to f32) -- ov::pass::MarkDequantization (registered in
// translate_session.cpp) marks the chain so it survives model-build-time ConstantFolding -- see
// make_int8_weights.cpp/make_int4_weights.cpp. mul_mat_id.cpp constructs ov::op::internal::GatherMatmul
// directly from the resulting f16 dequant chain.
//
// When use_bias is true (explicitly, or implicitly because for_gather_matmul is true), the zp
// tensor is expected to hold an exact f16 bias value (rather than a rounded integer zero point);
// it is converted in place into an exact zero_point = -bias/scale and consumed via Subtract, not
// Add, so the chain still matches OpenVINO's Convert->Subtract->Multiply decompression pattern.
ov::Output<ov::Node> make_int8_weights(ov::Tensor & weight,
ov::Tensor & scales,
ov::Tensor & zp,
size_t group_size = GGML_QUANTIZATION_GROUP_SIZE,
bool use_bias = false);
bool use_bias = false,
bool for_gather_matmul = false);
ov::Output<ov::Node> make_int4_weights(ov::Tensor & weight,
ov::Tensor & scales,
ov::Tensor & zp,
size_t group_size = GGML_QUANTIZATION_GROUP_SIZE,
bool use_bias = false);
bool use_bias = false,
bool for_gather_matmul = false);
ov::Output<ov::Node> make_mxfp4_weights(ov::Tensor & weight, ov::Tensor & scales);
ov::Output<ov::Node> make_mxfp4_moe_packed_weights(ov::Tensor & weight);
// Extract quantized weights from tensor and create weight subgraph
// If weights/scales/zp are provided (non-empty), uses them as output buffers
@@ -73,7 +93,9 @@ std::shared_ptr<ov::Node> extract_quantized_weights(
ov::Tensor & weights,
ov::Tensor & scales,
ov::Tensor & zp,
bool use_bias = false); // Use fp bias instead of quantized zero_point (for test-backend-ops)
bool use_bias = false); // Use an exact f16 zero point (vs. a rounded integer one); always
// used for for_gather_matmul (3D MoE expert) weights regardless of
// this flag, and also settable explicitly for test-backend-ops.
// Requantize weights from tensor to target format, writing to provided buffers
// For F16 target, only weights buffer is used (scales/zp ignored)
@@ -126,7 +148,10 @@ OvWeight process_weight_tensor(
const ggml_tensor * tensor,
const void * data, // Source data pointer (may differ from tensor->data)
void * output_base_ptr = nullptr, // Base pointer for output buffers (or nullptr for internal allocation)
bool use_bias = false); // Use fp bias instead of quantized zero_point, only used in test-backend-ops
bool use_bias = false); // Use an exact f16 zero point (vs. a rounded integer one);
// always used for for_gather_matmul (3D MoE expert) weights
// regardless of this flag, and also settable explicitly for
// test-backend-ops.
void quantize_q4_0(const float * x,
ov::Tensor & weights_arr,
@@ -139,13 +164,15 @@ void quantize_q8_1(const float * x,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk);
int64_t qk,
int64_t block_offset = 0);
void quantize_q8_0(const float * x,
ov::Tensor & weights_arr,
ov::Tensor & scales_arr,
ov::Tensor & zp_arr,
int64_t k,
int64_t qk);
int64_t qk,
int64_t block_offset = 0);
namespace ov {
namespace op {
+272
View File
@@ -0,0 +1,272 @@
#include "model-cache.h"
#include "ggml-backend-impl.h"
#include "ggml-backend.h"
#include "ggml-impl.h"
#include "ggml-openvino-extra.h"
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <openvino/core/version.hpp>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <vector>
#if defined(_WIN32)
# include <direct.h>
#endif
namespace {
// 64-bit FNV-1a, the mixing primitive for all fingerprints here.
inline uint64_t fnv1a(uint64_t h, const void * data, size_t n) {
const uint8_t * p = static_cast<const uint8_t *>(data);
for (size_t i = 0; i < n; ++i) {
h ^= p[i];
h *= 0x100000001b3ull;
}
return h;
}
inline uint64_t fnv1a_u64(uint64_t h, uint64_t v) {
return fnv1a(h, &v, sizeof(v));
}
constexpr uint64_t FNV_OFFSET = 0xcbf29ce484222325ull;
// Bytes sampled from each end of a weight tensor for the sampled hash. The whole
// model is never hashed (that would cost seconds every run); instead we sample a
// bounded window from the head and tail of each weight's bytes. The manifest
// re-verify (same sample) guards the residual collision risk.
constexpr size_t WEIGHT_SAMPLE_BYTES = 4096;
// Is this src a model weight, mirroring create_weight_nodes()'s selection:
// non-view tensor whose buffer is USAGE_WEIGHTS or whose type is quantized.
bool is_weight_src(const ggml_tensor * src) {
if (src == nullptr || src->view_src != nullptr || src->buffer == nullptr) {
return false;
}
return src->buffer->usage == GGML_BACKEND_BUFFER_USAGE_WEIGHTS || ggml_is_quantized(src->type);
}
// Per-weight sampled fingerprint: identity (name/shape/type) + a bounded byte
// sample. Returns FNV offset basis if data is unavailable (kept deterministic).
uint64_t weight_fingerprint(const ggml_tensor * t) {
uint64_t h = FNV_OFFSET;
h = fnv1a(h, t->name, strlen(t->name));
for (int i = 0; i < GGML_MAX_DIMS; ++i) {
h = fnv1a_u64(h, static_cast<uint64_t>(t->ne[i]));
}
h = fnv1a_u64(h, static_cast<uint64_t>(t->type));
const size_t nbytes = ggml_nbytes(t);
h = fnv1a_u64(h, nbytes);
if (t->data != nullptr && nbytes > 0) {
const size_t head = nbytes < WEIGHT_SAMPLE_BYTES ? nbytes : WEIGHT_SAMPLE_BYTES;
h = fnv1a(h, t->data, head);
if (nbytes > WEIGHT_SAMPLE_BYTES) {
const size_t tail = nbytes < 2 * WEIGHT_SAMPLE_BYTES ? nbytes - WEIGHT_SAMPLE_BYTES : WEIGHT_SAMPLE_BYTES;
h = fnv1a(h, static_cast<const uint8_t *>(t->data) + (nbytes - tail), tail);
}
}
return h;
}
// Walk the cgraph and invoke fn(weight_tensor) for each distinct weight, in node
// order. De-duplicates by tensor pointer so a weight used by several nodes is
// fingerprinted once, deterministically.
template <typename F>
void for_each_weight(const ggml_cgraph * cgraph, F && fn) {
std::vector<const ggml_tensor *> seen;
for (int i = 0; i < cgraph->n_nodes; ++i) {
const ggml_tensor * node = cgraph->nodes[i];
for (int s = 0; s < GGML_MAX_SRC; ++s) {
const ggml_tensor * src = node->src[s];
if (!is_weight_src(src)) {
continue;
}
bool dup = false;
for (const auto * p : seen) {
if (p == src) {
dup = true;
break;
}
}
if (dup) {
continue;
}
seen.push_back(src);
fn(src);
}
}
}
std::string ov_version_string() {
const ov::Version v = ov::get_openvino_version();
return std::string(v.buildNumber ? v.buildNumber : "unknown");
}
std::string hex64(uint64_t v) {
char buf[17];
snprintf(buf, sizeof(buf), "%016llx", static_cast<unsigned long long>(v));
return std::string(buf);
}
// Portable mkdir for a single path component. Returns true if the directory
// exists after the call (created now or already present).
bool make_dir(const std::string & path) {
#if defined(_WIN32)
int rc = _mkdir(path.c_str());
#else
int rc = ::mkdir(path.c_str(), 0755);
#endif
if (rc == 0 || errno == EEXIST) {
return true;
}
return false;
}
// Create `path` and any missing parents (like `mkdir -p`). Best-effort:
// returns true only if the full directory exists afterwards.
bool make_dirs(const std::string & path) {
if (path.empty()) {
return false;
}
std::string acc;
for (size_t i = 0; i < path.size(); ++i) {
const char c = path[i];
acc.push_back(c);
const bool sep = (c == '/'
#if defined(_WIN32)
|| c == '\\'
#endif
);
// Create each intermediate component (skip a leading "/" root).
if (sep && acc.size() > 1) {
std::string component = acc.substr(0, acc.size() - 1);
if (!make_dir(component)) {
return false;
}
}
}
return make_dir(path);
}
} // namespace
std::string ggml_openvino_model_cache_dir() {
const char * dir = ggml_openvino_getenv_str("GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR");
if (!dir || strlen(dir) == 0) {
return std::string();
}
std::string path(dir);
// Create the cache directory (and parents) on first use so callers don't
// have to pre-create it; a missing dir would otherwise silently disable the
// cache (manifest/blob writes fail with no directory to write into).
if (!make_dirs(path)) {
GGML_LOG_WARN("ggml-openvino: could not create model cache dir '%s' (errno=%d); caching disabled\n",
path.c_str(), errno);
return std::string();
}
return path;
}
uint64_t ggml_openvino_model_fingerprint(const ggml_cgraph * cgraph,
const std::string & device,
bool fa,
const int32_t * rope_params,
int rope_len,
uint64_t extra_cfg) {
uint64_t h = FNV_OFFSET;
// Topology: node count + each node's op and name (cheap, and distinguishes
// graphs that share weights but differ structurally).
h = fnv1a_u64(h, static_cast<uint64_t>(cgraph->n_nodes));
for (int i = 0; i < cgraph->n_nodes; ++i) {
const ggml_tensor * node = cgraph->nodes[i];
h = fnv1a_u64(h, static_cast<uint64_t>(node->op));
h = fnv1a(h, node->name, strlen(node->name));
}
// Weights: the model identity.
for_each_weight(cgraph, [&](const ggml_tensor * t) { h = fnv1a_u64(h, weight_fingerprint(t)); });
// Config that changes the produced blob.
h = fnv1a(h, device.data(), device.size());
h = fnv1a_u64(h, fa ? 1u : 0u);
if (rope_params && rope_len > 0) {
h = fnv1a(h, rope_params, sizeof(int32_t) * static_cast<size_t>(rope_len));
}
h = fnv1a_u64(h, extra_cfg);
const std::string ver = ov_version_string();
h = fnv1a(h, ver.data(), ver.size());
return h;
}
std::string ggml_openvino_model_cache_blob_path(const std::string & dir, uint64_t fingerprint) {
return dir + "/" + hex64(fingerprint) + ".blob";
}
std::string ggml_openvino_model_cache_manifest_path(const std::string & dir, uint64_t fingerprint) {
return dir + "/" + hex64(fingerprint) + ".manifest";
}
bool ggml_openvino_model_cache_write_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint) {
std::ofstream f(path, std::ios::trunc);
if (!f.is_open()) {
return false;
}
f << "fingerprint " << hex64(fingerprint) << "\n";
f << "ov_version " << ov_version_string() << "\n";
for_each_weight(cgraph, [&](const ggml_tensor * t) {
f << t->name << " " << t->ne[0] << " " << t->ne[1] << " " << t->ne[2] << " " << t->ne[3] << " "
<< static_cast<int>(t->type) << " " << hex64(weight_fingerprint(t)) << "\n";
});
return f.good();
}
bool ggml_openvino_model_cache_verify_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint) {
std::ifstream f(path);
if (!f.is_open()) {
return false;
}
std::string tag, val;
// header: fingerprint
if (!(f >> tag >> val) || tag != "fingerprint" || val != hex64(fingerprint)) {
return false;
}
// header: ov_version
if (!(f >> tag >> val) || tag != "ov_version" || val != ov_version_string()) {
return false;
}
// Build the expected per-weight lines from the live cgraph, then require an
// exact match (same set, same order) against the manifest.
std::vector<std::string> expected;
for_each_weight(cgraph, [&](const ggml_tensor * t) {
expected.push_back(std::string(t->name) + " " + std::to_string(t->ne[0]) + " " + std::to_string(t->ne[1]) +
" " + std::to_string(t->ne[2]) + " " + std::to_string(t->ne[3]) + " " +
std::to_string(static_cast<int>(t->type)) + " " + hex64(weight_fingerprint(t)));
});
size_t idx = 0;
std::string line;
std::getline(f, line); // consume rest of ov_version line
while (std::getline(f, line)) {
if (line.empty()) {
continue;
}
if (idx >= expected.size() || line != expected[idx]) {
return false;
}
++idx;
}
return idx == expected.size();
}
+56
View File
@@ -0,0 +1,56 @@
#pragma once
// Frontend-level compiled-model cache (GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR).
//
// The OpenVINO plugin's own ov::cache_dir caches the compiled blob keyed by the
// *OV model*, but producing that model still runs the full frontend every time:
// weight requantization (incl. the large token_embd F32 transient) and the
// ggml->OV graph conversion. This cache keys off a fingerprint computed directly
// from the ggml cgraph, so a hit skips requant + convert + compile entirely and
// instead imports a previously exported CompiledModel blob.
//
// Opt-in and independent from GGML_OPENVINO_CACHE_DIR. Default off.
#include "ggml.h"
#include <cstdint>
#include <string>
// Returns the compiled-model cache directory from GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR,
// or empty if unset/disabled. When empty, callers must not use the cache.
std::string ggml_openvino_model_cache_dir();
// Compute a stable 64-bit fingerprint identifying the model+config that a cgraph
// would compile to. Combines graph topology, a sampled hash of every weight
// tensor (name/shape/dtype + bounded byte sample), and the config that changes
// the produced blob (device, flash-attention, rope params, the compile-memory
// flags, stateful, and the OpenVINO version). `device` is the resolved device
// string; `fa` is the flash-attention flag; `rope_params`/`rope_len` cover the
// model's rope configuration; `extra_cfg` folds in any other blob-affecting bits.
uint64_t ggml_openvino_model_fingerprint(const ggml_cgraph * cgraph,
const std::string & device,
bool fa,
const int32_t * rope_params,
int rope_len,
uint64_t extra_cfg);
// Path to the compiled-blob file for a fingerprint (<dir>/<hex>.blob).
std::string ggml_openvino_model_cache_blob_path(const std::string & dir, uint64_t fingerprint);
// Path to the sidecar manifest (<dir>/<hex>.manifest) holding the per-weight
// fingerprints, used to re-verify a hit before trusting the blob.
std::string ggml_openvino_model_cache_manifest_path(const std::string & dir, uint64_t fingerprint);
// Write/read the manifest. The manifest is a newline-separated list of
// "name ne0 ne1 ne2 ne3 type sample_hash" lines plus a header line with the
// fingerprint and OV version. Returns false on I/O error.
bool ggml_openvino_model_cache_write_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint);
// Verify that the cgraph's weights still match the stored manifest (guards the
// sampled-hash collision risk: a blob is only trusted if every weight's
// name/shape/type/sample-hash matches what was cached). Returns true on match.
bool ggml_openvino_model_cache_verify_manifest(const std::string & path,
const ggml_cgraph * cgraph,
uint64_t fingerprint);
+22 -3
View File
@@ -6,12 +6,25 @@
#include <openvino/core/partial_shape.hpp>
#include <openvino/core/shape.hpp>
#include <openvino/frontend/decoder.hpp>
#include <set>
#include <string>
namespace ov {
namespace frontend {
namespace ggml {
struct ModelInputInfo {
element::Type type;
PartialShape shape;
};
struct ModelExtraInputInfo {
element::Type type;
Shape shape;
int64_t value;
bool is_parameter;
};
class GgmlDecoder : public DecoderBase {
public:
virtual ov::Any get_attribute(const std::string & name) const = 0;
@@ -75,6 +88,10 @@ public:
virtual std::vector<std::string> get_output_names(int node_idx) const = 0;
virtual std::string get_inplace_op_src(int node_idx) const = 0;
virtual bool is_view_like_alias_of(int node_idx, const std::string & view_src_name) const = 0;
virtual const std::string & get_op_type() const = 0;
virtual const std::string & get_op_type(int node_idx) const = 0;
@@ -87,15 +104,17 @@ public:
virtual int get_op_case(int node_idx) const = 0;
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_inputs() const = 0;
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_extra_inputs() const = 0;
virtual const std::map<std::string, ModelInputInfo> & get_model_inputs() const = 0;
virtual const std::map<std::string, ModelExtraInputInfo> & get_model_extra_inputs() const = 0;
virtual const std::map<std::string, std::shared_ptr<ov::Node>> & get_model_weights() const = 0;
virtual std::vector<std::string> get_model_output_names() const = 0;
virtual std::set<std::string> get_model_output_names() const = 0;
virtual int32_t * get_rope_params() const = 0;
virtual bool has_mixed_rope_params() const = 0;
virtual int get_ssm_state_size() const = 0;
virtual std::map<std::string, std::string> get_kv_param_res_names() const = 0;
virtual bool is_static() const = 0;
@@ -153,6 +153,8 @@ public:
bool is_stateful() const { return m_decoder->is_stateful(); }
int get_ssm_state_size() const { return m_decoder->get_ssm_state_size(); }
private:
std::shared_ptr<GgmlDecoder> m_decoder;
std::shared_ptr<TensorMap> & m_tensor_map;
@@ -0,0 +1,45 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_add(const NodeContext & context) {
num_inputs_check(context, 2, 2);
if (context.get_op_case() == 1) {
// MoE expert-plane sum (see is_moe_expert_sum_add): input 1 is a VIEW plane of the
// shared base tensor `experts` = [n_embd, n_expert_used, n_tokens, 1] (ggml order) ->
// [1, n_tokens, n_expert_used, n_embd] (OV order). The whole ADD chain is equivalent to
// reducing the expert axis (OV axis 2) of that base, so bypass the chain and the
// per-plane Slices entirely.
size_t view_size = context.get_view_input_size(1);
auto base_name = context.get_view_input_src_name(1, view_size - 1);
auto base = context.get_input(base_name);
auto reduced = std::make_shared<ov::op::v1::ReduceSum>(
base, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}), false);
auto res =
std::make_shared<ov::op::v0::Unsqueeze>(reduced, ov::op::v0::Constant::create(ov::element::i64, {1}, {1}));
return rename_outputs_with_suffix({res}, context.get_name());
}
auto input_0 = process_view_input_new(context, 0);
auto input_1 = process_view_input_new(context, 1);
auto res = std::make_shared<ov::op::v1::Add>(input_0, input_1);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
+153 -4
View File
@@ -2,10 +2,19 @@
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <memory>
#include <vector>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
namespace ov {
namespace frontend {
@@ -13,18 +22,158 @@ namespace ggml {
namespace op {
OutputVector translate_cpy(const NodeContext & context) {
auto input = process_view_input_new(context, 0);
auto op_case = context.get_op_case();
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_output_shape();
auto output_shape = context.get_input_shape(1);
if (op_case == 4) {
auto src = process_view_input_new(context, 0);
auto base = context.get_input(1);
int64_t n_elems = 1;
for (const auto & dim : context.get_output_shape().to_shape()) {
n_elems *= static_cast<int64_t>(dim);
}
const auto output_stride = context.get_output_stride();
const size_t elem_size = output_stride.empty() ? context.get_output_type().size() : output_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0, "CPY conv state view update has invalid element size");
const int64_t begin_val = static_cast<int64_t>(context.get_output_op_offset() / elem_size);
const int64_t end_val = begin_val + n_elems;
auto flat_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, -1});
src = std::make_shared<ov::op::v1::Reshape>(src, flat_shape, false);
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {begin_val});
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {end_val});
auto int_max = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto head_part = std::make_shared<ov::op::v8::Slice>(base, zero, begin, one, axis);
auto tail_part = std::make_shared<ov::op::v8::Slice>(base, end, int_max, one, axis);
auto res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{head_part, src, tail_part}, 3);
return rename_outputs_with_suffix({res}, context.get_name());
}
// Recurrent state cache writeback into a slot block of the cache. Where the block starts and
// where the copied data starts in the source are runtime inputs, so the cached model works for
// any kv head, active sequence count and token count. The result is the full updated cache.
// op_case 1: gated-delta-net state, op_case 2: conv state, op_case 3: defrag remainder.
const std::string slot_begin_name = "rs_slot_begin_" + context.get_name();
const bool slice_assign =
context.has_input(slot_begin_name) && !context.is_stateful() && (op_case >= 1 && op_case <= 3);
if (slice_assign) {
const int64_t slot_axis = 2;
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto int_max = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {slot_axis});
auto feature = ov::op::v0::Constant::create(ov::element::i64, {4},
std::vector<int64_t>{1, 1, -1, output_shape[3].get_length()});
ov::Output<ov::Node> src;
ov::Output<ov::Node> begin = context.get_input(slot_begin_name);
auto base = context.get_input(1);
if (op_case == 1) {
// GDN packs [attn | state snapshots]; the state part runs from src_begin to the end.
auto src_begin = context.get_input("rs_src_begin_" + context.get_name());
auto state_part = std::make_shared<ov::op::v8::Slice>(context.get_input(0), src_begin, int_max, one, axis);
src = std::make_shared<ov::op::v1::Reshape>(state_part, feature, false);
} else if (op_case == 2) {
// conv_input is [previous conv state | new tokens]; copy the conv_kernel_size - 1 wide
// window starting at src_begin, which is the snapshot this writeback corresponds to.
auto window_size = (int64_t) input_shape[3].get_length();
auto src_begin = context.get_input("rs_src_begin_" + context.get_name());
auto src_end = std::make_shared<ov::op::v1::Add>(
src_begin, ov::op::v0::Constant::create(ov::element::i64, {1}, {window_size}));
auto window = std::make_shared<ov::op::v8::Slice>(context.get_input(0), src_begin, src_end, one,
ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
const auto base_shape = base.get_partial_shape();
FRONT_END_OP_CONVERSION_CHECK(base_shape.rank().is_static() && base_shape.rank().get_length() == 4,
"CPY conv state cache update requires rank-4 base cache");
FRONT_END_OP_CONVERSION_CHECK(base_shape[3].is_static(),
"CPY conv state cache update requires static feature size");
FRONT_END_OP_CONVERSION_CHECK(input_shape.rank().is_static() && input_shape.rank().get_length() == 4 &&
input_shape[2].is_static() && input_shape[3].is_static(),
"CPY conv state cache update requires static source feature view");
const int64_t full_feature_size = base_shape[3].get_length();
const int64_t update_feature_size = input_shape[2].get_length() * input_shape[3].get_length();
const auto output_stride = context.get_output_stride();
const size_t elem_size = output_stride.empty() ? context.get_output_type().size() : output_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0,
"CPY conv state cache update has invalid element size");
const int64_t feature_begin = static_cast<int64_t>(context.get_output_op_offset() / elem_size) %
full_feature_size;
const int64_t feature_end = feature_begin + update_feature_size;
FRONT_END_OP_CONVERSION_CHECK(feature_begin >= 0 && feature_end <= full_feature_size,
"CPY conv state cache update feature range is out of bounds");
auto partial_feature = ov::op::v0::Constant::create(
ov::element::i64, {4}, std::vector<int64_t>{1, 1, -1, update_feature_size});
src = std::make_shared<ov::op::v1::Reshape>(window, partial_feature, false);
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
auto src_len = std::make_shared<ov::op::v8::Gather>(
std::make_shared<ov::op::v3::ShapeOf>(src, ov::element::i64), axis,
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
auto slot_end = std::make_shared<ov::op::v1::Add>(begin, src_len);
auto active_slots = std::make_shared<ov::op::v8::Slice>(base, begin, slot_end, one, axis);
auto feature_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto feature_begin_node = ov::op::v0::Constant::create(ov::element::i64, {1}, {feature_begin});
auto feature_end_node = ov::op::v0::Constant::create(ov::element::i64, {1}, {feature_end});
auto feature_head = std::make_shared<ov::op::v8::Slice>(active_slots, zero, feature_begin_node, one,
feature_axis);
auto feature_tail = std::make_shared<ov::op::v8::Slice>(active_slots, feature_end_node, int_max, one,
feature_axis);
src = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{feature_head, src, feature_tail}, 3);
} else {
// op_case 3: gathered remainder rows already have the cache slot layout [1, 1, extra, feature]
src = context.get_input(0);
}
if (src.get_element_type() != context.get_output_type()) {
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
}
auto src_len =
std::make_shared<ov::op::v8::Gather>(std::make_shared<ov::op::v3::ShapeOf>(src, ov::element::i64), axis,
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
auto end = std::make_shared<ov::op::v1::Add>(begin, src_len);
auto head_part = std::make_shared<ov::op::v8::Slice>(base, zero, begin, one, axis);
auto tail_part = std::make_shared<ov::op::v8::Slice>(base, end, int_max, one, axis);
auto res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{head_part, src, tail_part}, slot_axis);
return rename_outputs_with_suffix({res}, context.get_name());
}
auto input = process_view_input_new(context, 0);
// Non-cast CPY may need a reshape (e.g. [3,192,1,1] -> [576,1,1,1])
if (input_shape != output_shape) {
auto new_shape = ov::op::v0::Constant::create(
ov::element::i64, {static_cast<size_t>(output_shape.rank().get_length())}, output_shape.to_shape());
input = std::make_shared<ov::op::v1::Reshape>(input, new_shape, false);
}
auto res = std::make_shared<ov::op::v0::Convert>(input, context.get_output_type());
ov::Output<Node> res;
if (context.get_input_type(0) != context.get_output_type()) {
res = std::make_shared<ov::op::v0::Convert>(input, context.get_output_type());
} else {
res = input;
}
if (res.get_node_shared_ptr() == context.get_input(0).get_node_shared_ptr()) {
return {res};
}
return rename_outputs_with_suffix({res}, context.get_name());
}
@@ -0,0 +1,29 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/cum_sum.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML cumsum computes prefix sum along dim 0 (the innermost/fastest dimension).
// In OV layout the dims are reversed: ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0],
// so ggml dim 0 maps to OV axis 3 (last axis).
OutputVector translate_cumsum(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = context.get_input(0);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {3});
auto res = std::make_shared<ov::op::v0::CumSum>(x, axis);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,58 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/equal.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/select.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML DIAG takes a 1D vector (ne0, 1, ne2, ne3) and produces a diagonal matrix
// of shape (ne0, ne0, ne2, ne3).
// In OV layout (ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0]):
// input: [ne3, ne2, 1, ne0]
// output: [ne3, ne2, ne0, ne0]
// The diagonal: output[..., i, j] = input[..., 0, j] if i == j, else 0.
OutputVector translate_diag(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = context.get_input(0); // OV shape: [ne3, ne2, 1, ne0]
auto out_shape = context.get_output_shape().to_shape();
int64_t n = static_cast<int64_t>(out_shape[3]); // ne0
// Build index range [0, 1, ..., n-1]
auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(0)});
auto stop = ov::op::v0::Constant::create(ov::element::i64, {}, {n});
auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(1)});
auto range = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64);
// col_idx shape [1, 1, 1, n]
auto col_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, n});
auto col_idx = std::make_shared<ov::op::v1::Reshape>(range, col_shape, false);
// row_idx shape [1, 1, n, 1]
auto row_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, n, 1});
auto row_idx = std::make_shared<ov::op::v1::Reshape>(range, row_shape, false);
// mask: true where col == row (diagonal)
auto mask = std::make_shared<ov::op::v1::Equal>(col_idx, row_idx);
// Broadcast input from [ne3, ne2, 1, ne0] to [ne3, ne2, ne0, ne0] via select
auto zero = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto res = std::make_shared<ov::op::v1::Select>(mask, x, zero);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,34 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/broadcast.hpp>
#include <openvino/op/constant.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML FILL sets all elements of a tensor to a constant value.
// The constant is stored as a float in op_params[0].
OutputVector translate_fill(const NodeContext & context) {
num_inputs_check(context, 1, 1);
float c;
memcpy(&c, context.get_output_op_params(), sizeof(float));
auto shape = context.get_input_shape(0).to_shape();
auto val = ov::op::v0::Constant::create(ov::element::f32, {}, {c});
auto target_shape = ov::op::v0::Constant::create(ov::element::i64, {shape.size()},
std::vector<int64_t>(shape.begin(), shape.end()));
auto res = std::make_shared<ov::op::v3::Broadcast>(val, target_shape);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -19,6 +19,7 @@
#include <openvino/op/reshape.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/tile.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
@@ -31,57 +32,76 @@ namespace op {
static OutputVector translate_gated_delta_net_ref(const NodeContext & context);
OutputVector translate_gated_delta_net(const NodeContext & context) {
// auto v_shape = context.get_input_shape(2).to_shape(); // [B, T, H_v, S_v]
// auto q_shape = context.get_input_shape(0).to_shape(); // [B, T, H_k, S_k]
auto v_shape = context.get_input_shape(2).to_shape(); // [B, T, H_v, S_v]
auto q_shape = context.get_input_shape(0).to_shape(); // [B, T, H_k, S_k]
// // Fused GatedDeltaNet op only supports scalar gate (kda=0).
// // Fall back to reference implementation for per-key-dimension gating.
// // if (kda) {
// // return translate_gated_delta_net_ref(context);
// // }
// auto q = context.get_input(0);
// auto k = context.get_input(1);
// auto v = context.get_input(2);
// auto g = context.get_input(3);
// auto beta = context.get_input(4);
// auto state = context.get_input(5);
// Fused GatedDeltaNet op only supports scalar gate (kda=0).
// Fall back to reference implementation for per-key-dimension gating.
// if (kda) {
// return translate_gated_delta_net_ref(context);
// }
// const int64_t B = v_shape[0];
// const int64_t T = v_shape[1];
// const int64_t H_v = v_shape[2];
// const int64_t S_v = v_shape[3];
const int64_t H_v = v_shape[2];
const int64_t S_v = v_shape[3];
const int64_t H_k = q_shape[2];
// const int64_t S_k = q_shape[3];
// // ggml state layout (OV notation): [B, H_v, value_dim, key_dim]
// // GatedDeltaNet op expects: [B, H_v, key_dim, value_dim]
// auto state_reshape_shape =
// ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{B, H_v, S_v, S_k});
// state = std::make_shared<ov::op::v1::Reshape>(state, state_reshape_shape, false);
// auto state_perm = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 1, 3, 2});
// state = std::make_shared<ov::op::v1::Transpose>(state, state_perm);
auto q = context.get_input(0);
auto k = context.get_input(1);
auto v = process_view_input(context, 2, H_v * S_v);
auto g = context.get_input(3);
auto beta = context.get_input(4);
auto state = context.get_input(5);
// g = std::make_shared<ov::op::v0::Squeeze>(g, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
// beta = std::make_shared<ov::op::v0::Squeeze>(beta, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
// ggml maps GQA heads in tiled order, while OV GDN maps repeated heads in grouped order.
if (H_v != H_k) {
const int64_t repeat = H_v / H_k;
auto repeats = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, repeat, 1});
q = std::make_shared<ov::op::v0::Tile>(q, repeats);
k = std::make_shared<ov::op::v0::Tile>(k, repeats);
}
// auto gdn = std::make_shared<ov::op::internal::GatedDeltaNet>(q, k, v, state, g, beta);
if (context.get_view_input_size(2)) {
// Same as l2_norm case 1
v = std::make_shared<ov::op::v0::Squeeze>(v, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
auto v_shape = context.get_input_shape(2).to_shape();
std::vector<int64_t> reshape_pattern = {0, 0, (int64_t) v_shape[2], (int64_t) v_shape[3]};
v = std::make_shared<ov::op::v1::Reshape>(
v, ov::op::v0::Constant::create(ov::element::i64, {4}, reshape_pattern), true);
}
// auto attn_4d = gdn->output(0);
// auto state_4d = gdn->output(1); // [B, H_v, key_dim, value_dim]
// // Transpose output state back to ggml layout [B, H_v, value_dim, key_dim]
// auto state_transposed = std::make_shared<ov::op::v1::Transpose>(state_4d, state_perm);
// auto flat_shape_1d = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
// auto attn = std::make_shared<ov::op::v1::Reshape>(attn_4d, flat_shape_1d, false);
// auto new_state = std::make_shared<ov::op::v1::Reshape>(state_transposed, flat_shape_1d, false);
// auto packed = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{attn, new_state}, 0);
// auto out_shape =
// ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, T * B + S_v * B, S_v * H_v});
// auto res = std::make_shared<ov::op::v1::Reshape>(packed, out_shape, false);
// ggml state layout (OV notation): [B, H_v, value_dim, key_dim]
// GatedDeltaNet op expects: [B, H_v, key_dim, value_dim]
auto state_perm = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 1, 3, 2});
state = std::make_shared<ov::op::v1::Transpose>(state, state_perm);
// return rename_outputs_with_suffix({res}, context.get_name());
g = std::make_shared<ov::op::v0::Squeeze>(g, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
beta = std::make_shared<ov::op::v0::Squeeze>(beta, ov::op::v0::Constant::create(ov::element::i64, {1}, {3}));
// The OV version in CI does not have the GatedDeltaNet op, so use reference implementation for now.
return translate_gated_delta_net_ref(context);
// std::cout << "GatedDeltaNet input shapes: q=" << q.get_partial_shape() << ", k=" << k.get_partial_shape()
// << ", v=" << v.get_partial_shape() << ", g=" << g.get_partial_shape()
// << ", beta=" << beta.get_partial_shape() << ", state=" << state.get_partial_shape() << std::endl;
auto gdn = std::make_shared<ov::op::internal::GatedDeltaNet>(q, k, v, state, g, beta);
auto attn_4d = gdn->output(0);
auto state_4d = gdn->output(1); // [B, H_v, key_dim, value_dim]
// std::cout << "GatedDeltaNet output shapes: attn=" << gdn->output(0).get_partial_shape()
// << ", new_state=" << gdn->output(1).get_partial_shape() << std::endl;
// Transpose output state back to ggml layout [B, H_v, value_dim, key_dim]
auto state_transposed = std::make_shared<ov::op::v1::Transpose>(state_4d, state_perm);
auto flat_shape_1d = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto attn = std::make_shared<ov::op::v1::Reshape>(attn_4d, flat_shape_1d, false);
auto new_state = std::make_shared<ov::op::v1::Reshape>(state_transposed, flat_shape_1d, false);
auto packed = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{attn, new_state}, 0);
auto out_shape = ov::op::v0::Constant::create(ov::element::i64, {4},
std::vector<int64_t>{1, 1, -1 /*T * B + S_v * B*/, S_v * H_v});
auto res = std::make_shared<ov::op::v1::Reshape>(packed, out_shape, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
static OutputVector translate_gated_delta_net_ref(const NodeContext & context) {
@@ -0,0 +1,43 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's internal ov::op::internal::GatherMatmul op.
//
// The op class body (validate_and_infer_types / clone_with_new_inputs) is
// provided by the linked libopenvino.so; only the declaration is needed here so
// the backend can construct the node directly (same approach as GatedDeltaNet).
// The class layout must stay in sync with
// openvino/src/common/transformations/include/ov_ops/gather_matmul.hpp
//
// \note GatherMatmul op class is under development and subject to change.
#pragma once
#include "openvino/op/op.hpp"
namespace ov::op::internal {
class OPENVINO_API GatherMatmul : public ov::op::Op {
public:
OPENVINO_OP("GatherMatmul")
GatherMatmul() = default;
GatherMatmul(const ov::Output<Node>& A,
const ov::Output<Node>& B,
const ov::Output<Node>& indices,
const ov::Output<Node>& bias);
GatherMatmul(const ov::Output<Node>& A, const ov::Output<Node>& B, const ov::Output<Node>& indices);
std::shared_ptr<Node> clone_with_new_inputs(const ov::OutputVector& new_args) const override;
void validate_and_infer_types() override;
private:
// the weights matrix B is expected to have the transposed form [group, N, K]
static constexpr bool transp_a = false;
static constexpr bool transp_b = true;
};
} // namespace ov::op::internal
@@ -2,11 +2,16 @@
#include "../op_table.h"
#include "../utils.h"
#include <climits>
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/unsqueeze.hpp>
@@ -20,7 +25,27 @@ OutputVector translate_get_rows(const NodeContext & context) {
Output<Node> res;
auto data = process_view_input_new(context, 0);
auto indices = process_view_input_new(context, 1);
auto op_case = context.get_op_case();
ov::Output<ov::Node> indices;
if ((op_case == 1 || op_case == 2) && context.has_input("s_copy_active_slot_len")) {
// Recurrent state reorder (inp->s_copy): slice the active (op_case 1) or extra (op_case 2)
// segment from the s_copy index list at runtime, instead of baking the static view offset,
// so the cached IR works for any number of active sequences.
auto s_copy = context.get_input(1);
auto len = context.get_input("s_copy_active_slot_len");
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
if (op_case == 1) {
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
indices = std::make_shared<ov::op::v8::Slice>(s_copy, begin, len, step, axis);
} else {
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {INT_MAX});
indices = std::make_shared<ov::op::v8::Slice>(s_copy, len, end, step, axis);
}
} else {
indices = process_view_input_new(context, 1);
}
// data[1,b,x,y] ind[1,1,b,x'] test-backend-ops case
// data[x,y] ind[1,1,1,x'] normal case
@@ -37,7 +62,62 @@ OutputVector translate_get_rows(const NodeContext & context) {
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1});
data =
std::make_shared<ov::op::v0::Squeeze>(data, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
// data: [batch, rows, ...], indices: [batch, n] - this is a batched gather
// (batch_dims=1) along the rows axis. The data and indices batch dims are
// logically equal (both == n_tokens) but reach this node through independent
// reshapes, so the GPU plugin's gather shape inference cannot prove
// data.shape[0] == indices.shape[0] and rejects the node. We must tie both
// batch dims to the SAME value, and crucially that value must stay DYNAMIC.
const auto data_ps = data.get_partial_shape();
const auto idx_ps = indices.get_partial_shape();
const bool data_batch_static = data_ps.rank().is_static() && data_ps[0].is_static();
const bool idx_batch_dynamic = idx_ps.rank().is_dynamic() || idx_ps[0].is_dynamic();
if (data_batch_static && idx_batch_dynamic) {
// MoE per-expert-scale path: `data` is a statically-tiled REPEAT
// (ggml_repeat_4d(scale, 1, n_expert, n_tokens, 1)) whose batch dim is a
// compile-time-constant n_tokens, and every batch slice is IDENTICAL (it was
// tiled from a single [1, n_expert, 1] scale). `indices` (selected_experts)
// carries the genuinely dynamic token dim. Broadcasting indices up to the
// static data batch (the naive fix) would freeze the token dim to the
// captured prefill length, and that static value then flows through the
// gather into the residual stream, making every following decoder layer
// static -> triggers the GPU in-place-concat KV-cache corruption (only
// layer 0 stays dynamic). A static->dynamic Broadcast cannot expand, so
// instead collapse the redundant data batch to 1 and broadcast 1->dynamic to
// match the indices batch. Mathematically identical (the slices are equal),
// and the whole graph stays dynamic.
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto data_b1 = std::make_shared<ov::op::v8::Slice>(data, zero, one, one, axis0); // [1, rows, ...]
auto idx_shape = std::make_shared<ov::op::v3::ShapeOf>(indices, ov::element::i64);
auto idx_batch = get_dimensions(idx_shape, {0}); // [batch] (dynamic)
auto data_b1_shape = std::make_shared<ov::op::v3::ShapeOf>(data_b1, ov::element::i64);
const auto rank = data_ps.rank().get_length();
std::vector<int> rest_axes;
for (int a = 1; a < rank; ++a) {
rest_axes.push_back(a);
}
auto data_rest = get_dimensions(data_b1_shape, rest_axes); // [rows, ...]
auto data_target = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{idx_batch, data_rest}, 0);
data =
std::make_shared<ov::op::v3::Broadcast>(data_b1, data_target, ov::op::BroadcastType::BIDIRECTIONAL);
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
} else {
// General case: tie the indices batch to the data batch (the data batch is
// already dynamic, e.g. the routing-weights gather whose data comes from the
// activations). Broadcast indices to [data_batch, indices_n].
auto data_shape = std::make_shared<ov::op::v3::ShapeOf>(data, ov::element::i64);
auto data_batch = get_dimensions(data_shape, {0}); // [batch]
auto idx_shape = std::make_shared<ov::op::v3::ShapeOf>(indices, ov::element::i64);
auto idx_n = get_dimensions(idx_shape, {1}); // [n]
auto idx_target = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{data_batch, idx_n}, 0);
indices = std::make_shared<ov::op::v3::Broadcast>(indices, idx_target,
ov::op::BroadcastType::BIDIRECTIONAL);
res = std::make_shared<ov::op::v8::Gather>(data, indices, axis, 1);
}
}
} else if (context.is_stateful() && data.get_partial_shape().rank() == 3) {
auto axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {1});
@@ -8,7 +8,9 @@
#include <openvino/op/maximum.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/reduce_sum.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/sqrt.hpp>
#include <openvino/op/squeeze.hpp>
namespace ov {
namespace frontend {
@@ -20,6 +22,21 @@ OutputVector translate_l2_norm(const NodeContext & context) {
auto input_node = process_view_input_new(context, 0);
if (context.get_op_case() == 1) {
// 92: [ 128, 16, 1, 2] VIEW q_conv-1
// [ 6144, 1, 2, 1] 0: UNARY conv_output_silu-1
// 93: [ 128, 16, 1, 2] L2_NORM q_conv_predelta-1
// [ 128, 16, 1, 2] 0: VIEW q_conv-1
auto output_shape = context.get_output_shape().to_shape();
input_node = process_view_input(context, 0, output_shape[2] * output_shape[3]);
input_node =
std::make_shared<ov::op::v0::Squeeze>(input_node, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
std::vector<int64_t> reshape_pattern = {0, 0, (int64_t) output_shape[2], (int64_t) output_shape[3]};
input_node = std::make_shared<ov::op::v1::Reshape>(
input_node, ov::op::v0::Constant::create(ov::element::i64, {4}, reshape_pattern), true);
}
auto squared = std::make_shared<ov::op::v1::Multiply>(input_node, input_node);
auto sum_squared = std::make_shared<ov::op::v1::ReduceSum>(
+105 -48
View File
@@ -1,6 +1,8 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include "gather_matmul.hpp"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <cstdint>
#include <cstring>
@@ -18,6 +20,7 @@
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
@@ -37,6 +40,70 @@ ov::Output<ov::Node> slice_axis(const ov::Output<ov::Node> & input, int64_t axis
const_i64({axis}));
}
ov::Output<ov::Node> static_shape_dims_or_shapeof(const ov::Output<ov::Node> & input,
const std::vector<int> & dims) {
const auto partial_shape = input.get_partial_shape();
if (partial_shape.is_static()) {
std::vector<int64_t> values;
values.reserve(dims.size());
for (const int64_t dim : dims) {
values.push_back(partial_shape[dim].get_length());
}
return const_i64(values);
}
auto shape = std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64);
return get_dimensions(shape, dims);
}
ov::Output<ov::Node> translate_mul_mat_id_gather_matmul_fallback(const NodeContext & context,
ov::Output<ov::Node> expert_weights,
ov::Output<ov::Node> activations,
ov::Output<ov::Node> ids) {
auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
ov::Output<ov::Node> selected_weights = std::make_shared<ov::op::v8::Gather>(expert_weights, ids, gather_axis);
const auto output_type = context.get_output_type();
if (selected_weights.get_element_type() != ov::element::f32) {
selected_weights = std::make_shared<ov::op::v0::Convert>(selected_weights, ov::element::f32);
}
if (activations.get_element_type() != ov::element::f32) {
activations = std::make_shared<ov::op::v0::Convert>(activations, ov::element::f32);
}
auto activations_shape = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64);
auto ids_shape = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64);
ov::Output<ov::Node> acts_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{
get_dimensions(activations_shape, {0}),
get_dimensions(ids_shape, {1}),
get_dimensions(activations_shape, {2}),
},
0);
ov::Output<ov::Node> acts_broadcasted =
std::make_shared<ov::op::v3::Broadcast>(activations, acts_target_dims, ov::op::BroadcastType::BIDIRECTIONAL);
auto activations_expanded = std::make_shared<ov::op::v0::Unsqueeze>(acts_broadcasted, const_i64({2}));
ov::Output<ov::Node> result =
std::make_shared<ov::op::v0::MatMul>(activations_expanded, selected_weights, false, true);
auto output_shape = context.get_output_shape();
FRONT_END_OP_CONVERSION_CHECK(output_shape.rank().is_static() && output_shape.rank().get_length() == 4,
"Unexpected MUL_MAT_ID output rank");
FRONT_END_OP_CONVERSION_CHECK(output_shape[3].is_static(), "Expected static row dimension for MUL_MAT_ID output");
auto batch_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto row_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {output_shape[3].get_length()});
auto result_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{batch_dim, get_dimensions(ids_shape, {0, 1}), row_dim}, 0);
result = std::make_shared<ov::op::v1::Reshape>(result, result_target_dims, false);
if (result.get_element_type() != output_type) {
result = std::make_shared<ov::op::v0::Convert>(result, output_type);
}
return result;
}
ov::Output<ov::Node> translate_mul_mat_id_mxfp4_packed(const NodeContext & context,
ov::Output<ov::Node> expert_weights,
ov::Output<ov::Node> activations,
@@ -144,22 +211,33 @@ OutputVector translate_mul_mat_id(const NodeContext & context) {
context.get_name());
}
// General (non-packed) path: dense F32/F16/BF16 weights, or the f16 dequantization chain for
// quantized MoE experts (see extract_quantized_weights / make_int4_weights / make_int8_weights in
// ggml-quants.cpp). Routed through ov::op::internal::GatherMatmul instead of a naive
// Gather+Broadcast+MatMul, so the selected expert's full weight matrix is never materialized per
// token. The CPU plugin's ConvertGatherMatmulToGatherMatmulCompressed pass (run during
// compile_model) fuses the dequantization chain feeding GatherMatmul's B input into a
// GatherMatmulCompressed node automatically, as long as MarkDequantization has marked the chain --
// see translate_session.cpp's apply_transformations for the MarkDequantization registration.
//
// OpenVINO sees GGML tensors in reversed dimension order:
// weights: [1, n_expert, m, k]
// activations: [1, n_tokens, n_used_or_1, k]
// ids: [1, 1, n_tokens, n_used]
// Rebuild the logical ranks explicitly from the 4D inputs instead of relying
// on fixed squeeze axes: real graphs can arrive through VIEW/RESHAPE chains
// where singleton axes are still represented differently at this point.
auto expert_weights_shape_4d = std::make_shared<ov::op::v3::ShapeOf>(expert_weights, ov::element::i64);
auto activations_shape_4d = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64);
auto ids_shape_4d = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64);
// expert_weights is either [1, n_expert, m, k] (4D, e.g. non-quantized weights without a
// pre-built extra) or already [n_expert, m, k] (3D, weights routed through
// process_weight_tensor) -- GatherMatmul's B input expects the latter.
auto expert_weights_rank = expert_weights.get_partial_shape().rank();
FRONT_END_OP_CONVERSION_CHECK(expert_weights_rank.is_static(),
"Expected static rank for MUL_MAT_ID expert weights");
const bool use_gpu_fallback = ggml_openvino_get_device_name() == "GPU";
if (expert_weights_rank.get_length() == 4) {
auto expert_weights_shape_3d = static_shape_dims_or_shapeof(expert_weights, {1, 2, 3});
expert_weights = std::make_shared<ov::op::v1::Reshape>(expert_weights, expert_weights_shape_3d, false);
}
auto expert_weights_shape_3d = get_dimensions(expert_weights_shape_4d, {1, 2, 3});
auto activations_shape_3d = get_dimensions(activations_shape_4d, {1, 2, 3});
auto ids_shape_2d = get_dimensions(ids_shape_4d, {2, 3});
auto activations_shape_3d = static_shape_dims_or_shapeof(activations, {1, 2, 3});
auto ids_shape_2d = static_shape_dims_or_shapeof(ids, {2, 3});
expert_weights = std::make_shared<ov::op::v1::Reshape>(expert_weights, expert_weights_shape_3d, false);
activations = std::make_shared<ov::op::v1::Reshape>(activations, activations_shape_3d, false);
ids = std::make_shared<ov::op::v1::Reshape>(ids, ids_shape_2d, false);
@@ -167,51 +245,30 @@ OutputVector translate_mul_mat_id(const NodeContext & context) {
ids = std::make_shared<ov::op::v0::Convert>(ids, ov::element::i32);
}
auto gather_axis = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{}, {0});
ov::Output<ov::Node> selected_weights = std::make_shared<ov::op::v8::Gather>(expert_weights, ids, gather_axis);
const auto output_type = context.get_output_type();
if (selected_weights.get_element_type() != ov::element::f32) {
selected_weights = std::make_shared<ov::op::v0::Convert>(selected_weights, ov::element::f32);
}
if (activations.get_element_type() != ov::element::f32) {
activations = std::make_shared<ov::op::v0::Convert>(activations, ov::element::f32);
}
auto activations_shape = std::make_shared<ov::op::v3::ShapeOf>(activations, ov::element::i64);
auto ids_shape = std::make_shared<ov::op::v3::ShapeOf>(ids, ov::element::i64);
ov::Output<ov::Node> acts_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{
get_dimensions(activations_shape, {0}),
get_dimensions(ids_shape, {1}),
get_dimensions(activations_shape, {2}),
},
0);
ov::Output<ov::Node> acts_broadcasted =
std::make_shared<ov::op::v3::Broadcast>(activations, acts_target_dims, ov::op::BroadcastType::BIDIRECTIONAL);
if (use_gpu_fallback || !expert_weights.get_partial_shape().is_static() || !activations.get_partial_shape().is_static() ||
!ids.get_partial_shape().is_static()) {
return rename_outputs_with_suffix({translate_mul_mat_id_gather_matmul_fallback(context, expert_weights, activations, ids)},
context.get_name());
}
auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {2});
auto activations_expanded = std::make_shared<ov::op::v0::Unsqueeze>(acts_broadcasted, unsqueeze_axes);
// GatherMatmul's A input is [n_used_or_1, n_tokens, k]; activations_3d is
// [n_tokens, n_used_or_1, k].
auto activations_transpose_order = const_i64({1, 0, 2});
ov::Output<ov::Node> activations_for_gather =
std::make_shared<ov::op::v1::Transpose>(activations, activations_transpose_order);
auto batch_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto output_shape = context.get_output_shape();
FRONT_END_OP_CONVERSION_CHECK(output_shape.rank().is_static() && output_shape.rank().get_length() == 4,
"Unexpected MUL_MAT_ID output rank");
FRONT_END_OP_CONVERSION_CHECK(output_shape[3].is_static(), "Expected static row dimension for MUL_MAT_ID output");
const auto row_dim_value = output_shape[3].get_length();
auto row_dim = ov::op::v0::Constant::create(ov::element::i64, {1}, {row_dim_value});
ov::Output<ov::Node> result = std::make_shared<ov::op::internal::GatherMatmul>(activations_for_gather, expert_weights, ids);
ov::Output<ov::Node> result =
std::make_shared<ov::op::v0::MatMul>(activations_expanded, selected_weights, false, true);
auto result_target_dims = std::make_shared<ov::op::v0::Concat>(
ov::OutputVector{
batch_dim,
get_dimensions(ids_shape, {0, 1}),
row_dim,
},
0);
result = std::make_shared<ov::op::v1::Reshape>(result, result_target_dims, false);
// result is [n_used, n_tokens, m]; GGML expects [1, n_tokens, n_used, m].
auto result_transpose_order = const_i64({1, 0, 2});
result = std::make_shared<ov::op::v1::Transpose>(result, result_transpose_order);
auto unsqueeze_axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
result = std::make_shared<ov::op::v0::Unsqueeze>(result, unsqueeze_axes);
if (result.get_element_type() != output_type) {
result = std::make_shared<ov::op::v0::Convert>(result, output_type);
+10 -36
View File
@@ -23,47 +23,21 @@ OutputVector translate_repeat(const NodeContext & context) {
auto input = process_view_input_new(context, 0);
const auto input_shape = context.get_input_shape(0);
const auto output_shape = context.get_output_shape();
const auto input_shape = context.get_input_shape(0).to_shape();
const auto output_shape = context.get_output_shape().to_shape();
if (input_shape.rank().is_static() && output_shape.rank().is_static() &&
input_shape.rank() == output_shape.rank()) {
const auto rank = static_cast<size_t>(input_shape.rank().get_length());
std::vector<int64_t> repeats(rank, 1);
bool all_static = true;
std::vector<int64_t> repeats(4, 1);
for (size_t axis = 0; axis < 4; ++axis) {
const int64_t input_dim = input_shape[axis];
const int64_t output_dim = output_shape[axis];
for (size_t axis = 0; axis < rank; ++axis) {
if (!input_shape[axis].is_static() || !output_shape[axis].is_static()) {
all_static = false;
break;
}
FRONT_END_OP_CONVERSION_CHECK(input_dim > 0 && output_dim > 0 && output_dim % input_dim == 0,
"REPEAT input shape ", input_shape, " cannot tile to match ", output_shape);
const int64_t input_dim = input_shape[axis].get_length();
const int64_t output_dim = output_shape[axis].get_length();
FRONT_END_OP_CONVERSION_CHECK(input_dim > 0 && output_dim > 0 && output_dim % input_dim == 0,
"REPEAT input shape ", input_shape, " cannot tile to match ", output_shape);
repeats[axis] = output_dim / input_dim;
}
if (all_static) {
auto repeats_node = ov::op::v0::Constant::create(ov::element::i64, {repeats.size()}, repeats);
ov::Output<ov::Node> res = std::make_shared<ov::op::v0::Tile>(input, repeats_node);
return rename_outputs_with_suffix({res}, context.get_name());
}
repeats[axis] = output_dim / input_dim;
}
// Dynamic fallback: tile by the ratio of output to input shape.
auto input_shape_node = std::make_shared<ov::op::v3::ShapeOf>(input, ov::element::i64);
std::shared_ptr<ov::Node> target_shape_node;
if (output_shape.rank().is_static() && output_shape.is_static()) {
target_shape_node =
ov::op::v0::Constant::create(ov::element::i64, {output_shape.to_shape().size()}, output_shape.to_shape());
} else {
target_shape_node = std::make_shared<ov::op::v3::ShapeOf>(context.get_input(1), ov::element::i64);
}
auto repeats_node = std::make_shared<ov::op::v1::Divide>(target_shape_node, input_shape_node);
auto repeats_node = ov::op::v0::Constant::create(ov::element::i64, {repeats.size()}, repeats);
ov::Output<ov::Node> res = std::make_shared<ov::op::v0::Tile>(input, repeats_node);
return rename_outputs_with_suffix({res}, context.get_name());
}
+29 -6
View File
@@ -25,13 +25,12 @@ OutputVector translate_reshape(const NodeContext & context) {
}
int op_case = context.get_op_case();
FRONT_END_CHECK_IMPLEMENTED(
op_case == 1 || op_case == 2 || op_case == 3 || op_case == 4 || op_case == 5 || op_case == 6,
"Unsupported RESHAPE case");
auto output_shape = context.get_output_shape().to_shape();
std::shared_ptr<ov::Node> new_shape_node;
if (op_case == 1) {
if (op_case == 0) {
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {4}, context.get_output_shape().to_shape());
} else if (op_case == 1) {
if (context.is_stateful()) {
new_shape_node = ov::op::v0::Constant::create(
ov::element::i64, {3}, std::vector<int64_t>{-1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
@@ -76,9 +75,33 @@ OutputVector translate_reshape(const NodeContext & context) {
// ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) context.get_output_shape().to_shape()[3]});
// auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
// new_shape_node = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{one, one, token_len, emb_size}, 0);
} else if (op_case == 6) {
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {4}, context.get_output_shape().to_shape());
// 14: [ 6144, 1, 2, 1] RESHAPE linear_attn_qkv_mixed-0
// [ 6144, 2, 1, 1] 0: MUL_MAT node_13
// reshape to [1, n_slot_active_len, -1, 6144]
if (context.has_input("s_copy_active_slot_len")) {
auto n_slot_active_len = context.get_input("s_copy_active_slot_len");
auto emb_size = ov::op::v0::Constant::create(ov::element::i64, {1},
{(int64_t) context.get_output_shape().to_shape()[3]});
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto neg_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
new_shape_node =
std::make_shared<ov::op::v0::Concat>(ov::OutputVector{one, n_slot_active_len, neg_one, emb_size}, 0);
} else {
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {4}, context.get_output_shape().to_shape());
}
} else if (op_case == 7) {
// 57: [ 2048, 2, 1, 1] RESHAPE linear_attn_out-0 (reshaped)
// [ 2048, 1, 2, 1] 0: MUL_MAT linear_attn_out-0
std::vector<int64_t> shape_vec = {1, 1, -1, (int64_t) context.get_output_shape().to_shape()[3]};
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {4}, shape_vec);
} else if (op_case == 8) {
// 106: [ 128, 128, 16, 2] RESHAPE state_predelta-1
// [ 262144, 2, 1, 1] 0: GET_ROWS node_86
auto output_shape = context.get_output_shape().to_shape();
std::vector<int64_t> shape_vec = {-1, (int64_t) output_shape[1], (int64_t) output_shape[2],
(int64_t) output_shape[3]};
new_shape_node = ov::op::v0::Constant::create(ov::element::i64, {4}, shape_vec);
}
auto res = std::make_shared<ov::op::v1::Reshape>(context.get_input(0), new_shape_node, false);
return rename_outputs_with_suffix({res}, context.get_name());
@@ -7,8 +7,11 @@
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/power.hpp>
#include <openvino/op/reduce_mean.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/sqrt.hpp>
namespace ov {
@@ -19,9 +22,41 @@ namespace op {
OutputVector translate_rms_norm(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input_node = process_view_input_new(context, 0);
auto square = std::make_shared<ov::op::v1::Power>(
input_node, ov::op::v0::Constant::create(ov::element::f32, ov::Shape{1}, {2.0f}));
auto op_case = context.get_op_case();
ov::Output<ov::Node> input_node;
if (op_case == 1) {
input_node = process_view_input_new(context, 0);
} else if (op_case == 2) {
auto ssm_state_size = context.get_ssm_state_size();
// The GDN op packs [attn | new_state] along the row axis; the state occupies the last
// ssm_state_size * n_seqs rows. Slice it off (scaling by the active sequence count) to keep
// just the attention output.
ov::Output<ov::Node> state_end;
if (context.has_input("s_copy_active_slot_len")) {
auto len = context.get_input("s_copy_active_slot_len");
auto state_rows = std::make_shared<ov::op::v1::Multiply>(
ov::op::v0::Constant::create(ov::element::i64, {1}, {ssm_state_size}), len);
state_end = std::make_shared<ov::op::v0::Negative>(state_rows);
} else {
state_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {-ssm_state_size});
}
auto gdn_attn_output = std::make_shared<ov::op::v8::Slice>(
context.get_input(0), ov::op::v0::Constant::create(ov::element::i64, {1}, {0}), state_end,
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {2}));
auto input_shape = context.get_input_shape(0).to_shape();
input_node = std::make_shared<ov::op::v1::Reshape>(
gdn_attn_output,
ov::op::v0::Constant::create(
ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) input_shape[2], (int64_t) input_shape[3]}),
false);
} else {
input_node = process_view_input_new(context, 0);
}
auto square = std::make_shared<ov::op::v1::Multiply>(input_node, input_node);
auto mean = std::make_shared<ov::op::v1::ReduceMean>(
square, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {-1}), true);
+84 -26
View File
@@ -22,6 +22,7 @@
#include <openvino/op/subtract.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <openvino/op/variadic_split.hpp>
#include <vector>
namespace ov {
@@ -40,6 +41,9 @@ OutputVector translate_rope(const NodeContext & context) {
auto output_shape = context.get_output_shape().to_shape();
int32_t * op_params = context.get_output_op_params();
const int mode = op_case;
const int64_t head_dim = static_cast<int64_t>(output_shape[3]);
const int64_t configured_n_dims = static_cast<int64_t>(op_params[1]);
const int64_t n_dims = configured_n_dims == 0 ? head_dim : configured_n_dims;
constexpr int TYPE_NORMAL = 0;
constexpr int TYPE_NEOX = 1;
@@ -80,6 +84,9 @@ OutputVector translate_rope(const NodeContext & context) {
data_node = std::make_shared<ov::op::v0::Convert>(data_node, ov::element::f32);
}
FRONT_END_OP_CONVERSION_CHECK(n_dims > 0 && n_dims <= head_dim && (n_dims % 2 == 0),
"ROPE expects even n_dims in [1, head_dim]");
// TODO(openvino-gpu-rope-fusion): TEMPORARY WORKAROUND - do NOT revert until the
// OpenVINO GPU plugin is updated.
//
@@ -94,13 +101,18 @@ OutputVector translate_rope(const NodeContext & context) {
// be restored to the captured even/odd translation. Until then, keep both paths:
// the active Flux rewrite here and the previous translation preserved below.
if (mode == TYPE_NORMAL) {
auto axis_last = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto zero = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto step_one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
// Emit the Flux-style interleaved-RoPE pattern so the GPU plugin's
// RoPEFusionFlux matcher folds this subgraph into ov::op::internal::RoPE:
// x_paired = Reshape(x, [1, S, n_heads, head_size/2, 2])
// x_paired = Reshape(x_rot, [1, S, n_heads, n_dims/2, 2])
// x0, x1 = Split(x_paired, axis=-1, num_splits=2)
// x1_neg = x1 * -1
// x_rotated = Reshape(Concat([x1_neg, x0], axis=-1), [1, S, n_heads, head_size])
// y = x * t_cos + x_rotated * t_sin
// x_rotated = Reshape(Concat([x1_neg, x0], axis=-1), [1, S, n_heads, n_dims])
// y_rot = x_rot * t_cos + x_rotated * t_sin
// y = Concat([y_rot, x_tail], axis=-1) if n_dims < head_dim
// Mathematically equivalent to the even/odd Slice form below.
//
// RoPEFusionFlux requires rank_equals(4) on x, t_cos and t_sin. The cos/sin
@@ -114,15 +126,16 @@ OutputVector translate_rope(const NodeContext & context) {
std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false);
}
const int64_t head_size = static_cast<int64_t>(output_shape[3]);
const int64_t n_heads = static_cast<int64_t>(output_shape[2]);
const int64_t half = head_size / 2;
const int64_t half = n_dims / 2;
auto rot_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims});
auto rot_data = std::make_shared<ov::op::v8::Slice>(data_node, zero, rot_end, step_one, axis_last);
auto neg_one_f = ov::op::v0::Constant::create(data_node->get_element_type(), ov::Shape{}, {-1.0f});
auto paired_shape =
ov::op::v0::Constant::create(ov::element::i64, {5}, std::vector<int64_t>{1, -1, n_heads, half, 2});
auto x_paired = std::make_shared<ov::op::v1::Reshape>(data_node, paired_shape, false);
auto paired_shape = ov::op::v0::Constant::create(
ov::element::i64, {5}, std::vector<int64_t>{1, -1, n_heads, half, 2});
auto x_paired = std::make_shared<ov::op::v1::Reshape>(rot_data, paired_shape, false);
auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1});
auto data_split = std::make_shared<ov::op::v1::Split>(x_paired, split_axis, 2);
@@ -133,28 +146,38 @@ OutputVector translate_rope(const NodeContext & context) {
auto x_rotated_paired = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{x1_neg, x0}, -1);
auto flat_shape =
ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, -1, n_heads, head_size});
auto x_rotated = std::make_shared<ov::op::v1::Reshape>(x_rotated_paired, flat_shape, false);
ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, -1, n_heads, n_dims});
auto x_rotated =
std::make_shared<ov::op::v1::Reshape>(x_rotated_paired, flat_shape, false);
// Expand cos/sin from [..., head_size/2] to [..., head_size] by repeating each
// Expand cos/sin from [..., n_dims/2] to [..., n_dims] by repeating each
// entry twice. Use special_zero on the final Reshape so the seq dim passes
// through dynamically. Final rank is 4 to satisfy the matcher's predicate.
auto expand_cos_sin = [&](Output<Node> cs) {
auto cs_unsq =
std::make_shared<ov::op::v0::Unsqueeze>(cs, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}));
auto bcast_target =
ov::op::v0::Constant::create(ov::element::i64, {5}, std::vector<int64_t>{1, 1, 1, half, 2});
auto bcast =
std::make_shared<ov::op::v3::Broadcast>(cs_unsq, bcast_target, ov::op::BroadcastType::BIDIRECTIONAL);
auto flat = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 0, 0, head_size});
auto cs_unsq = std::make_shared<ov::op::v0::Unsqueeze>(
cs, ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}));
auto bcast_target = ov::op::v0::Constant::create(
ov::element::i64, {5}, std::vector<int64_t>{1, 1, 1, half, 2});
auto bcast = std::make_shared<ov::op::v3::Broadcast>(
cs_unsq, bcast_target, ov::op::BroadcastType::BIDIRECTIONAL);
auto flat = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{0, 0, 0, n_dims});
return std::make_shared<ov::op::v1::Reshape>(bcast, flat, true);
};
Output<Node> cos_full = expand_cos_sin(cos_theta_node);
Output<Node> sin_full = expand_cos_sin(sin_theta_node);
auto y1 = std::make_shared<ov::op::v1::Multiply>(data_node, cos_full);
auto y1 = std::make_shared<ov::op::v1::Multiply>(rot_data, cos_full);
auto y2 = std::make_shared<ov::op::v1::Multiply>(x_rotated, sin_full);
res = std::make_shared<ov::op::v1::Add>(y1, y2);
auto rotated = std::make_shared<ov::op::v1::Add>(y1, y2);
if (n_dims < head_dim) {
auto tail_start = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_dims});
auto tail_end = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_dim});
auto tail = std::make_shared<ov::op::v8::Slice>(data_node, tail_start, tail_end, step_one, axis_last);
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{rotated, tail}, -1);
} else {
res = rotated;
}
}
// PRESERVED PREVIOUS TRANSLATION - Re-enable this branch (and remove the Flux branch above) once
// the GPU plugin's RoPE fusion is updated to recognize the even/odd Slice form;
@@ -196,8 +219,27 @@ OutputVector translate_rope(const NodeContext & context) {
// ov::element::i64, {4}, std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
// res = std::make_shared<ov::op::v1::Reshape>(stack, data_shape, false);
else if (mode == TYPE_NEOX) {
auto data_split = std::make_shared<ov::op::v1::Split>(
data_node, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1}), 2);
// In stateful mode the data arrives rank-3 ([S, n_heads, head_size]) while the
// cos/sin tables are rank-4 ([1, S, 1, n_dims/2]). The resulting mixed-rank
// broadcast in the Multiply below is miscomputed by the OpenVINO GPU plugin,
// corrupting the rotated Q/K. Lift the data to rank-4 ([1, S, n_heads, head_size])
// first so the RoPE Multiplies are equal-rank, matching the TYPE_NORMAL branch.
// Stateful RoPE already produced rank-4 output, so downstream attention is unaffected.
if (context.is_stateful()) {
auto r4_shape = ov::op::v0::Constant::create(
ov::element::i64, {4},
std::vector<int64_t>{1, -1, (int64_t) output_shape[2], (int64_t) output_shape[3]});
data_node = std::make_shared<ov::op::v1::Reshape>(data_node, r4_shape, false);
}
auto axis_last = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {-1});
std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2};
if (n_dims < head_dim) {
split_lengths.push_back(head_dim - n_dims);
}
auto data_split = std::make_shared<ov::op::v1::VariadicSplit>(
data_node, axis_last,
ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths));
Output<Node> slice_data_node_0 = data_split->outputs()[0];
Output<Node> slice_data_node_1 = data_split->outputs()[1];
@@ -209,16 +251,27 @@ OutputVector translate_rope(const NodeContext & context) {
std::make_shared<ov::op::v1::Multiply>(slice_data_node_0, sin_theta_node),
std::make_shared<ov::op::v1::Multiply>(slice_data_node_1, cos_theta_node));
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node}, -1);
if (n_dims < head_dim) {
Output<Node> tail = data_split->outputs()[2];
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node, tail}, -1);
} else {
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{first_half_node, second_half_node}, -1);
}
} else if (mode == TYPE_IMROPE) {
int64_t n_dims = data_node->get_output_partial_shape(0)[3].get_length();
auto cos_sin_shape = std::make_shared<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4},
std::vector<int64_t>{1, -1, 1, (n_dims >> 1)});
auto cos_reshaped = std::make_shared<ov::op::v1::Reshape>(cos_theta_node, cos_sin_shape, true);
auto sin_reshaped = std::make_shared<ov::op::v1::Reshape>(sin_theta_node, cos_sin_shape, true);
auto split_axis = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {3});
auto split_a = std::make_shared<ov::op::v1::Split>(data_node, split_axis, 2);
std::vector<int64_t> split_lengths = {n_dims / 2, n_dims / 2};
if (n_dims < head_dim) {
split_lengths.push_back(head_dim - n_dims);
}
auto split_a = std::make_shared<ov::op::v1::VariadicSplit>(
data_node, split_axis,
ov::op::v0::Constant::create(ov::element::i64, {split_lengths.size()}, split_lengths));
auto x0 = split_a->output(0);
auto x1 = split_a->output(1);
auto mul_a = std::make_shared<ov::op::v1::Multiply>(x0, cos_reshaped);
@@ -229,7 +282,12 @@ OutputVector translate_rope(const NodeContext & context) {
auto mul_d = std::make_shared<ov::op::v1::Multiply>(x1, cos_reshaped);
auto add = std::make_shared<ov::op::v1::Add>(mul_c, mul_d);
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add}, 3);
if (n_dims < head_dim) {
auto tail = split_a->output(2);
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add, tail}, 3);
} else {
res = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{sub, add}, 3);
}
}
if (res.get_element_type() != output_type) {
@@ -2,9 +2,24 @@
#include "../op_table.h"
#include "../utils.h"
#include <openvino/core/except.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/equal.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/if.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/logical_or.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/unsqueeze.hpp>
#include <vector>
namespace ov {
@@ -21,6 +36,36 @@ OutputVector translate_scale(const NodeContext & context) {
memcpy(&bias, (float *) context.get_output_op_params() + 1, sizeof(float));
auto scale_node = std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{}, std::vector<float>{scale});
if (context.get_op_case() == 1 && context.has_input("cache_rs_reset_len")) {
auto cache_rs_reset_idx = context.get_input("cache_rs_reset_idx");
auto cache_rs_reset_len = context.get_input("cache_rs_reset_len");
auto cache_rs = context.get_input(0);
auto cache_shape = std::make_shared<ov::op::v3::ShapeOf>(cache_rs, ov::element::i64);
auto n_slots_1d = std::make_shared<ov::op::v8::Gather>(
cache_shape, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {2}),
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}));
auto n_slots = std::make_shared<ov::op::v0::Squeeze>(n_slots_1d);
auto iota = std::make_shared<ov::op::v4::Range>(
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {0}), n_slots,
ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {1}), ov::element::i64);
auto idx_plus_len = std::make_shared<ov::op::v1::Add>(cache_rs_reset_idx, cache_rs_reset_len);
auto less_than_idx = std::make_shared<ov::op::v1::Less>(iota, cache_rs_reset_idx);
auto greater_equal_idx_plus_len = std::make_shared<ov::op::v1::GreaterEqual>(iota, idx_plus_len);
auto keep_mask = std::make_shared<ov::op::v1::LogicalOr>(less_than_idx, greater_equal_idx_plus_len);
auto keep_mask_f32 = std::make_shared<ov::op::v0::Convert>(keep_mask, ov::element::f32);
auto keep_mask_reshape = std::make_shared<ov::op::v0::Unsqueeze>(
keep_mask_f32, ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, {1}));
auto cleared_cache_rs = std::make_shared<ov::op::v1::Multiply>(cache_rs, keep_mask_reshape);
return rename_outputs_with_suffix({cleared_cache_rs}, context.get_name());
}
auto scaled = std::make_shared<ov::op::v1::Multiply>(context.get_input(0), scale_node);
std::shared_ptr<ov::Node> res;
@@ -0,0 +1,76 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <cstdint>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reduce_prod.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML SET writes src1 into a view of src0 and returns the updated tensor.
OutputVector translate_set(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto dst = process_view_input_new(context, 0);
auto src = process_view_input_new(context, 1);
src = std::make_shared<ov::op::v0::Convert>(src, context.get_output_type());
const auto dst_stride = context.get_input_stride(0);
FRONT_END_OP_CONVERSION_CHECK(dst_stride.size() >= 4, "SET requires 4D destination strides");
const auto * op_params = reinterpret_cast<const uint32_t *>(context.get_output_op_params());
const size_t offset = static_cast<size_t>(op_params[3]);
const size_t elem_size = dst_stride.back();
FRONT_END_OP_CONVERSION_CHECK(elem_size != 0 && offset % elem_size == 0,
"SET offset must be aligned to destination element size");
const int64_t offset_elems = static_cast<int64_t>(offset / elem_size);
auto dst_flat = std::make_shared<ov::op::v1::Reshape>(
dst,
ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}),
false);
auto src_flat = std::make_shared<ov::op::v1::Reshape>(
src,
ov::op::v0::Constant::create(ov::element::i64, {1}, {-1}),
false);
auto src_shape = std::make_shared<ov::op::v3::ShapeOf>(src_flat, ov::element::i64);
auto src_len = std::make_shared<ov::op::v1::ReduceProd>(
src_shape,
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}),
false);
auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {offset_elems});
auto stop = std::make_shared<ov::op::v1::Add>(start, src_len);
auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {1});
auto indices = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {}, {0});
auto updated_flat = std::make_shared<ov::op::v3::ScatterUpdate>(dst_flat, indices, src_flat, axis);
auto dst_shape = std::make_shared<ov::op::v3::ShapeOf>(dst, ov::element::i64);
auto res = std::make_shared<ov::op::v1::Reshape>(updated_flat, dst_shape, false);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
+30 -11
View File
@@ -8,11 +8,13 @@
#include <openvino/core/node.hpp>
#include <openvino/core/node_output.hpp>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_elements_update.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
@@ -29,20 +31,17 @@ OutputVector translate_set_rows(const NodeContext & context) {
num_inputs_check(context, 3, 3);
auto data = process_view_input_new(context, 0);
auto indices = context.get_input(1);
auto dst = context.get_input(2);
auto indices = process_view_input_new(context, 1);
auto dst = process_view_input_new(context, 2);
data = std::make_shared<ov::op::v0::Convert>(data, context.get_output_type());
auto row_size = context.get_input_shape(2)[3].get_length();
const auto indices_shape = context.get_input_shape(1);
const bool multidim_indices = indices_shape.rank().is_static() &&
indices_shape.rank().get_length() == 4 &&
((indices_shape[1].is_static() && indices_shape[1].get_length() > 1) ||
(indices_shape[2].is_static() && indices_shape[2].get_length() > 1));
auto ind_squeezed =
std::make_shared<ov::op::v0::Squeeze>(indices, ov::op::v0::Constant::create(ov::element::i64, {3}, {0, 1, 2}));
auto data_reshaped = std::make_shared<ov::op::v1::Reshape>(
data,
ov::op::v0::Constant::create(ov::element::i64, {4},
{(int64_t) 1, (int64_t) 1, (int64_t) -1, (int64_t) row_size}),
false);
auto axes = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{}, {2});
Output<Node> res;
@@ -53,11 +52,31 @@ OutputVector translate_set_rows(const NodeContext & context) {
data = std::make_shared<ov::op::v1::Reshape>(
data, ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 1, (int64_t) -1, dim2, dim3}), false);
res = std::make_shared<ov::op::v0::Concat>(OutputVector{dst, data}, concat_axis);
} else if (multidim_indices) {
auto updates_shape = std::make_shared<ov::op::v3::ShapeOf>(data, ov::element::i64);
auto indices_rank3 = std::make_shared<ov::op::v0::Squeeze>(
indices, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
auto one = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto indices_rank4_shape = std::make_shared<ov::op::v0::Concat>(OutputVector{get_dimensions(updates_shape, {0, 1, 2}), one}, 0);
auto indices_rank4 = std::make_shared<ov::op::v1::Reshape>(indices_rank3, indices_rank4_shape, false);
auto broadcasted_indices = std::make_shared<ov::op::v3::Broadcast>(indices_rank4, updates_shape);
res = std::make_shared<ov::op::v3::ScatterElementsUpdate>(dst, broadcasted_indices, data, axes);
} else {
auto row_size = context.get_input_shape(2)[3].get_length();
auto ind_squeezed = std::make_shared<ov::op::v0::Squeeze>(
indices, ov::op::v0::Constant::create(ov::element::i64, {3}, {0, 1, 2}));
auto data_reshaped = std::make_shared<ov::op::v1::Reshape>(
data,
ov::op::v0::Constant::create(ov::element::i64, {4},
{(int64_t) 1, (int64_t) 1, (int64_t) -1, (int64_t) row_size}),
false);
res = std::make_shared<ov::op::v3::ScatterUpdate>(dst, ind_squeezed, data_reshaped, axes);
}
if (auto dst_reshape = std::dynamic_pointer_cast<ov::op::v1::Reshape>(dst.get_node_shared_ptr())) {
auto dst_reshape = std::dynamic_pointer_cast<ov::op::v1::Reshape>(dst.get_node_shared_ptr());
if (!multidim_indices && dst_reshape) {
// Fix the case of multiple sequences, reshape back to original shape [1, n_seq, ctx_per_seq, emb]
// ctx_per_seq is not fixed due to llama-bench compatibility
auto dst_shape_partial = dst_reshape->get_input_partial_shape(0);
@@ -0,0 +1,108 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/broadcast.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/loop.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/subtract.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML SOLVE_TRI: solve Ax = B for lower-triangular A via forward substitution.
// Currently only lower, right, non-unitriangular variant is implemented.
//
// ggml layout: A [n, n, B1, B2], B [k, n, B1, B2] → X [k, n, B1, B2]
// OV layout: A [B2, B1, n, n], B [B2, B1, n, k] → X [B2, B1, n, k]
//
// Forward substitution row i:
// x[i] = (b[i] - sum_{t<i} A[i,t]*x[t]) / A[i,i]
//
// Implemented as an OV Loop op iterating n times with a carried X accumulator.
// Key insight: A is lower-triangular and X starts as zeros, so the full matmul
// A_row_i @ X_partial = sum_{t<i} A[i,t]*x[t] exactly (upper triangle of A
// is zero; unfilled rows of X are zero).
OutputVector translate_solve_tri(const NodeContext & context) {
num_inputs_check(context, 2, 2);
auto A = context.get_input(0); // [B2, B1, n, n]
auto B = context.get_input(1); // [B2, B1, n, k]
auto A_shape = context.get_input_shape(0).to_shape();
int64_t n = static_cast<int64_t>(A_shape[2]);
// Initial X: zeros with shape of B
auto B_shape_node = std::make_shared<ov::op::v3::ShapeOf>(B, ov::element::i64);
auto zero_f32 = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto X_init = std::make_shared<ov::op::v3::Broadcast>(zero_f32, B_shape_node);
// --- Loop body parameters ---
// body_iter: iteration counter injected by the Loop op (i64, shape {1})
auto body_iter = std::make_shared<ov::op::v0::Parameter>(ov::element::i64, ov::Shape{1});
auto body_X = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto body_A = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto body_B_p = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, ov::PartialShape::dynamic(4));
auto c_axis2 = ov::op::v0::Constant::create(ov::element::i64, {1}, {int64_t(2)});
auto c_axis3 = ov::op::v0::Constant::create(ov::element::i64, {1}, {int64_t(3)});
auto c_axis2_scalar = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(2)});
// b_i = B[..., i, :] [B2, B1, 1, k]
auto b_i = std::make_shared<ov::op::v8::Gather>(body_B_p, body_iter, c_axis2);
// A_row_i = A[..., i, :] [B2, B1, 1, n]
auto A_row_i = std::make_shared<ov::op::v8::Gather>(body_A, body_iter, c_axis2);
// sum_i = A_row_i @ X [B2, B1, 1, k]
// (lower-tri zeros + unfilled-X zeros make this equal to the partial sum)
auto sum_i = std::make_shared<ov::op::v0::MatMul>(A_row_i, body_X, false, false);
// diag_i = A[..., i, i] [B2, B1, 1, 1]
auto diag_i = std::make_shared<ov::op::v8::Gather>(A_row_i, body_iter, c_axis3);
// x_i = (b_i - sum_i) / diag_i [B2, B1, 1, k]
auto x_i = std::make_shared<ov::op::v1::Divide>(
std::make_shared<ov::op::v1::Subtract>(b_i, sum_i), diag_i);
// X_updated: scatter x_i into body_X at row i along axis 2
auto X_updated = std::make_shared<ov::op::v3::ScatterUpdate>(body_X, body_iter, x_i, c_axis2_scalar);
auto body_cond = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, {true});
auto body = std::make_shared<ov::Model>(
ov::OutputVector{body_cond, X_updated},
ov::ParameterVector{body_iter, body_X, body_A, body_B_p});
// --- Assemble Loop ---
auto trip_count = ov::op::v0::Constant::create(ov::element::i64, ov::Shape{1}, std::vector<int64_t>{n});
auto exec_cond = ov::op::v0::Constant::create(ov::element::boolean, ov::Shape{1}, {true});
auto loop = std::make_shared<ov::op::v5::Loop>(trip_count, exec_cond);
loop->set_function(body);
// iter_counter_body_param_idx=0 (body_iter), exec_condition_body_result_idx=0 (body_cond)
loop->set_special_body_ports(ov::op::v5::Loop::SpecialBodyPorts{0, 0});
// Carried state: X feeds back from X_updated each iteration
loop->set_merged_input(body_X, X_init, X_updated);
// Invariant inputs passed through unchanged
loop->set_invariant_input(body_A, A);
loop->set_invariant_input(body_B_p, B);
// Final output: value of X_updated after the last iteration
auto X_final = loop->get_iter_value(X_updated, -1);
return rename_outputs_with_suffix({X_final}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,35 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/op/multiply.hpp>
#include <openvino/op/sqrt.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_sqr(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v1::Multiply>(input, input);
return rename_outputs_with_suffix({res}, context.get_name());
}
OutputVector translate_sqrt(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto input = process_view_input_new(context, 0);
auto res = std::make_shared<ov::op::v0::Sqrt>(input);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -5,7 +5,9 @@
#include <openvino/op/constant.hpp>
#include <openvino/op/group_conv.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
namespace ov {
namespace frontend {
@@ -21,15 +23,15 @@ OutputVector translate_ssm_conv(const NodeContext & context) {
auto sx_shape = context.get_input_shape(0).to_shape(); // [1, n_s, d_inner, ncs]
auto c_shape = context.get_input_shape(1).to_shape(); // [1, 1, d_inner, d_conv]
int64_t n_s = sx_shape[1];
// int64_t n_s = sx_shape[1];
int64_t d_inner = sx_shape[2];
int64_t ncs = sx_shape[3]; // d_conv - 1 + n_t
int64_t d_conv = c_shape[3];
int64_t n_t = ncs - d_conv + 1;
// int64_t ncs = sx_shape[3]; // d_conv - 1 + n_t
int64_t d_conv = c_shape[3];
// int64_t n_t = ncs - d_conv + 1;
// Reshape sx from [1, n_s, d_inner, ncs] to [n_s, d_inner, ncs] for 1D GroupConvolution
auto sx_new_shape = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{n_s, d_inner, ncs});
auto sx_reshaped = std::make_shared<ov::op::v1::Reshape>(sx, sx_new_shape, false);
auto sx_reshaped =
std::make_shared<ov::op::v0::Squeeze>(sx, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
// Reshape c from [1, 1, d_inner, d_conv] to [d_inner, 1, 1, d_conv]
// GroupConvolution filter: [groups, out_channels/groups, in_channels/groups, kernel_size]
@@ -47,8 +49,8 @@ OutputVector translate_ssm_conv(const NodeContext & context) {
auto transposed = std::make_shared<ov::op::v1::Transpose>(conv, perm);
// Reshape to output shape [1, n_s, n_t, d_inner]
auto out_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, n_s, n_t, d_inner});
auto res = std::make_shared<ov::op::v1::Reshape>(transposed, out_shape, false);
auto res =
std::make_shared<ov::op::v0::Unsqueeze>(transposed, ov::op::v0::Constant::create(ov::element::i64, {1}, {0}));
return rename_outputs_with_suffix({res}, context.get_name());
}
@@ -0,0 +1,82 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/constant.hpp>
#include <openvino/op/greater.hpp>
#include <openvino/op/greater_eq.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/less_eq.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/select.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
// GGML TRI zeroes out elements outside a triangular region of a square matrix.
// The type param (stored in op_params[0]) maps to ggml_tri_type:
// 0 = UPPER_DIAG : keep where col >= row
// 1 = UPPER : keep where col > row
// 2 = LOWER_DIAG : keep where col <= row
// 3 = LOWER : keep where col < row
//
// In OV layout (ggml [ne0, ne1, ne2, ne3] → OV [ne3, ne2, ne1, ne0]):
// ggml dim 0 (ne0, cols) → OV axis 3
// ggml dim 1 (ne1, rows) → OV axis 2
// The matrix is square so ne0 == ne1.
OutputVector translate_tri(const NodeContext & context) {
num_inputs_check(context, 1, 1);
auto x = context.get_input(0); // OV shape: [ne3, ne2, ne1, ne0]
int32_t tri_type = context.get_output_op_params()[0];
auto shape = context.get_input_shape(0).to_shape();
int64_t n = static_cast<int64_t>(shape[3]); // ne0 == ne1
// Build index range [0, 1, ..., n-1]
auto start = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(0)});
auto stop = ov::op::v0::Constant::create(ov::element::i64, {}, {n});
auto step = ov::op::v0::Constant::create(ov::element::i64, {}, {int64_t(1)});
auto range = std::make_shared<ov::op::v4::Range>(start, stop, step, ov::element::i64);
// col_idx shape [1, 1, 1, n] — broadcasts over batch and row dims
auto col_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, 1, n});
auto col_idx = std::make_shared<ov::op::v1::Reshape>(range, col_shape, false);
// row_idx shape [1, 1, n, 1] — broadcasts over batch and col dims
auto row_shape = ov::op::v0::Constant::create(ov::element::i64, {4}, std::vector<int64_t>{1, 1, n, 1});
auto row_idx = std::make_shared<ov::op::v1::Reshape>(range, row_shape, false);
// Build boolean mask: true where element should be kept
std::shared_ptr<ov::Node> mask;
switch (tri_type) {
case 0: // UPPER_DIAG: col >= row
mask = std::make_shared<ov::op::v1::GreaterEqual>(col_idx, row_idx);
break;
case 1: // UPPER: col > row
mask = std::make_shared<ov::op::v1::Greater>(col_idx, row_idx);
break;
case 2: // LOWER_DIAG: col <= row
mask = std::make_shared<ov::op::v1::LessEqual>(col_idx, row_idx);
break;
case 3: // LOWER: col < row
mask = std::make_shared<ov::op::v1::Less>(col_idx, row_idx);
break;
default:
throw std::runtime_error("translate_tri: invalid tri_type " + std::to_string(tri_type));
}
auto zero = ov::op::v0::Constant::create(ov::element::f32, {}, {0.0f});
auto res = std::make_shared<ov::op::v1::Select>(mask, x, zero);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
+120
View File
@@ -1,8 +1,11 @@
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <set>
@@ -15,6 +18,123 @@ OutputVector translate_view(const NodeContext & context) {
num_inputs_check(context, 1, 1);
if (!context.is_static()) {
// On the stateless/non-static path VIEW is normally a no-op (consumers re-slice).
// EXCEPTION: the MoE expert aggregation slices each expert plane out of
// ffn_moe_weighted [n_embd, n_expert_used, n_tokens] with ggml_view_2d and then
// sums the planes with a chain of ADDs (llama-graph.cpp). Those ADDs read this
// VIEW node directly from the tensor map and do NOT re-slice, so a no-op here
// makes every plane the full tensor and the expert sum collapses. Materialize the
// single-expert slice here. Gated by name (ffn_moe_weighted...view) so it can't
// affect any other view.
const std::string & vname = context.get_name();
if (vname.find("ffn_moe_weighted") != std::string::npos) {
auto src_ps = context.get_input_shape(0);
auto dst_ps = context.get_output_shape();
if (src_ps.rank().is_static() && dst_ps.rank().is_static() && src_ps.rank() == dst_ps.rank() &&
src_ps.is_static() && dst_ps.is_static()) {
auto sst = context.get_input_stride(0);
auto dst = context.get_output_stride();
size_t voff = context.get_output_op_offset();
auto ss = src_ps.to_shape();
auto dd = dst_ps.to_shape();
const size_t nd = ss.size();
if (sst.size() == nd && dst.size() == nd) {
// Map each dst axis of size>1 to a src axis with equal (size,stride);
// the unmatched src axis of size>1 is the indexed expert axis.
// dst_to_src[d] records which src axis each dst axis came from, so we can
// later pull the dynamic (token) dim from the right source axis at runtime.
std::vector<bool> used(nd, false);
std::vector<int> dst_to_src(nd, -1);
bool ok = true;
for (size_t d = 0; d < nd; ++d) {
if (dd[d] == 1) {
continue;
}
int found = -1;
for (size_t s = 0; s < nd; ++s) {
if (!used[s] && ss[s] == dd[d] && sst[s] == dst[d]) {
found = (int) s;
break;
}
}
if (found < 0) {
ok = false;
break;
}
used[found] = true;
dst_to_src[d] = found;
}
int dropped = -1;
if (ok) {
for (size_t s = 0; s < nd; ++s) {
if (!used[s] && ss[s] > 1) {
if (dropped >= 0) {
ok = false;
break;
}
dropped = (int) s;
}
}
}
if (ok && dropped >= 0) {
const size_t dstr = sst[dropped];
const int64_t dsz = (int64_t) ss[dropped];
if (dstr > 0 && voff % dstr == 0) {
const int64_t sel = (int64_t) (voff / dstr);
if (sel >= 0 && sel < dsz) {
ov::Output<ov::Node> sl = std::make_shared<ov::op::v8::Slice>(
context.get_input(0),
ov::op::v0::Constant::create(ov::element::i64, {1}, {sel}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {sel + 1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {dropped}));
// Build the reshape target from the (concrete) dst shape, but
// keep the dynamic token axis dynamic instead of freezing it
// to the captured n_tokens. Without this the constant dst
// shape bakes in the prefill token count and the static value
// flows downstream, turning every later decoder layer static
// (the GPU in-place-concat KV-cache bug). The token axis is
// PERMUTED between the sliced input and the dst (e.g. input
// [1,tok,expert,emb] -> dst [1,1,tok,emb]), so special_zero
// (which copies the same-position dim) is not enough: pull the
// dynamic dim from the correct SOURCE axis via ShapeOf+Gather
// and place it at the dst token position.
const int32_t dyn = context.get_op_dynamic_dim(); // output ggml axis, -1 if none
int dst_ov_axis = (dyn != -1) ? (3 - (int) dyn) : -1; // get_shape() reverses ggml order
int src_ov_axis = (dst_ov_axis >= 0 && dst_ov_axis < (int) nd)
? dst_to_src[dst_ov_axis]
: -1;
if (dst_ov_axis >= 0 && src_ov_axis >= 0) {
// target = concat of per-axis scalars; the token axis is a
// runtime Gather of the slice's shape, the rest are constants.
auto sl_shape = std::make_shared<ov::op::v3::ShapeOf>(sl, ov::element::i64);
auto tok_dim = std::make_shared<ov::op::v8::Gather>(
sl_shape,
ov::op::v0::Constant::create(ov::element::i64, {1}, {src_ov_axis}),
ov::op::v0::Constant::create(ov::element::i64, {}, {0}));
ov::OutputVector parts;
for (int a = 0; a < (int) nd; ++a) {
if (a == dst_ov_axis) {
parts.push_back(tok_dim);
} else {
parts.push_back(ov::op::v0::Constant::create(
ov::element::i64, {1}, {(int64_t) dd[a]}));
}
}
auto dc = std::make_shared<ov::op::v0::Concat>(parts, 0);
auto rs = std::make_shared<ov::op::v1::Reshape>(sl, dc, false);
return rename_outputs_with_suffix({rs}, context.get_name());
}
auto dc = ov::op::v0::Constant::create(
ov::element::i64, {nd}, std::vector<int64_t>(dd.begin(), dd.end()));
auto rs = std::make_shared<ov::op::v1::Reshape>(sl, dc, false);
return rename_outputs_with_suffix({rs}, context.get_name());
}
}
}
}
}
}
return {context.get_input(0)};
}
+18 -1
View File
@@ -4,10 +4,13 @@
#include <openvino/op/add.hpp>
#include <openvino/op/divide.hpp>
#include <openvino/op/exp.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/gelu.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/tanh.hpp>
@@ -18,12 +21,13 @@ namespace ggml {
std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
using namespace ov::op;
return {
{"GGML_OP_ADD", op::translate_1to1_match_2_inputs<v1::Add> },
{"GGML_OP_ADD", op::translate_add },
{"GGML_OP_ADD1", op::translate_1to1_match_2_inputs<v1::Add> },
{"GGML_OP_ADD_ID", op::translate_add_id },
{"GGML_OP_CONCAT", op::translate_concat },
{"GGML_OP_CONT", op::translate_cont },
{"GGML_OP_DIV", op::translate_div },
{"GGML_OP_FILL", op::translate_fill },
{"GGML_OP_GET_ROWS", op::translate_get_rows },
{"GGML_OP_IM2COL", op::translate_im2col },
{"GGML_OP_MUL", op::translate_1to1_match_2_inputs<v1::Multiply>},
@@ -37,14 +41,20 @@ std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
{"GGML_OP_SUM_ROWS", op::translate_sum_rows },
{"GGML_OP_ROPE", op::translate_rope },
{"GGML_OP_SCALE", op::translate_scale },
{"GGML_OP_SQR", op::translate_sqr },
{"GGML_OP_SQRT", op::translate_sqrt },
{"GGML_OP_SOFT_MAX", op::translate_soft_max },
{"GGML_OP_ARGSORT", op::translate_argsort },
{"GGML_OP_SUB", op::translate_1to1_match_2_inputs<v1::Subtract>},
{"GGML_OP_TRANSPOSE", op::translate_transpose },
{"GGML_UNARY_OP_GELU", op::translate_1to1_match_1_input<v7::Gelu> },
{"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input<v0::Sigmoid> },
{"GGML_UNARY_OP_SILU", op::translate_unary_silu },
{"GGML_UNARY_OP_SOFTPLUS", op::translate_unary_softplus },
{"GGML_UNARY_OP_TANH", op::translate_1to1_match_1_input<v0::Tanh> },
{"GGML_UNARY_OP_SIGMOID", op::translate_1to1_match_1_input<v0::Sigmoid> },
{"GGML_UNARY_OP_EXP", op::translate_1to1_match_1_input<v0::Exp> },
{"GGML_UNARY_OP_NEG", op::translate_1to1_match_1_input<v0::Negative> },
{"GGML_OP_VIEW", op::translate_view },
{"GGML_GLU_OP_SWIGLU", op::translate_glu_swiglu },
{"GGML_GLU_OP_SWIGLU_OAI", op::translate_glu_swiglu_oai },
@@ -57,6 +67,13 @@ std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
{"GGML_OP_SSM_CONV", op::translate_ssm_conv },
{"GGML_OP_GATED_DELTA_NET", op::translate_gated_delta_net },
{"GGML_OP_REPEAT", op::translate_repeat },
{"GGML_OP_CUMSUM", op::translate_cumsum },
{"GGML_OP_FILL", op::translate_fill },
{"GGML_OP_DIAG", op::translate_diag },
{"GGML_OP_TRI", op::translate_tri },
{"GGML_OP_SET", op::translate_set },
// solve_tri has accuracy issues on GPU
// {"GGML_OP_SOLVE_TRI", op::translate_solve_tri },
};
}
@@ -10,10 +10,12 @@ namespace op {
#define GGML_OP_CONVERTER(op) OutputVector op(const NodeContext & context)
GGML_OP_CONVERTER(translate_add);
GGML_OP_CONVERTER(translate_cont);
GGML_OP_CONVERTER(translate_concat);
GGML_OP_CONVERTER(translate_add_id);
GGML_OP_CONVERTER(translate_div);
GGML_OP_CONVERTER(translate_fill);
GGML_OP_CONVERTER(translate_get_rows);
GGML_OP_CONVERTER(translate_im2col);
GGML_OP_CONVERTER(translate_mulmat);
@@ -24,8 +26,10 @@ GGML_OP_CONVERTER(translate_rms_norm);
GGML_OP_CONVERTER(translate_norm);
GGML_OP_CONVERTER(translate_l2_norm);
GGML_OP_CONVERTER(translate_sum_rows);
GGML_OP_CONVERTER(translate_sqr);
GGML_OP_CONVERTER(translate_rope);
GGML_OP_CONVERTER(translate_scale);
GGML_OP_CONVERTER(translate_sqrt);
GGML_OP_CONVERTER(translate_unary_silu);
GGML_OP_CONVERTER(translate_unary_softplus);
GGML_OP_CONVERTER(translate_soft_max);
@@ -43,6 +47,12 @@ GGML_OP_CONVERTER(translate_pad);
GGML_OP_CONVERTER(translate_ssm_conv);
GGML_OP_CONVERTER(translate_gated_delta_net);
GGML_OP_CONVERTER(translate_repeat);
GGML_OP_CONVERTER(translate_cumsum);
GGML_OP_CONVERTER(translate_fill);
GGML_OP_CONVERTER(translate_set);
GGML_OP_CONVERTER(translate_diag);
GGML_OP_CONVERTER(translate_tri);
GGML_OP_CONVERTER(translate_solve_tri);
} // namespace op
@@ -0,0 +1,44 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0
//
// Local mirror of OpenVINO's ov::pass::MarkDequantization pass declaration.
//
// The pass body is provided by the linked libopenvino.so; only the declaration is needed here so
// we can register it directly in our own TranslateSession::apply_transformations (same approach as
// MarkCompressedFloatConstants's local mirror in mark_decompression_convert_constant_folding.h). This
// lets us mark our GatherMatmul dequantization chain with disable_constant_folding regardless of the
// CPU/GPU plugin's own is_decompression_multiply() consumer allowlist.
// The class layout must stay in sync with
// openvino/src/common/transformations/include/transformations/low_precision/mark_dequantization_subgraph.hpp
#pragma once
#include "openvino/core/type/element_type.hpp"
#include "openvino/core/visibility.hpp"
#include "openvino/pass/matcher_pass.hpp"
#ifdef OPENVINO_STATIC_LIBRARY
# define TRANSFORMATIONS_API
#else
# ifdef IMPLEMENT_OPENVINO_API
# define TRANSFORMATIONS_API OPENVINO_CORE_EXPORTS
# else
# define TRANSFORMATIONS_API OPENVINO_CORE_IMPORTS
# endif // IMPLEMENT_OPENVINO_API
#endif // OPENVINO_STATIC_LIBRARY
namespace ov {
namespace pass {
class TRANSFORMATIONS_API MarkDequantization;
} // namespace pass
} // namespace ov
class ov::pass::MarkDequantization : public MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("MarkDequantization")
explicit MarkDequantization(const element::TypeVector & precisions,
bool fold_subtract_const = false,
bool fold_multiply_const = true);
};
@@ -1,18 +1,23 @@
#include "translate_session.h"
#include "ggml-impl.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include "ggml-openvino/openvino/node_context.h"
#include "ggml-openvino/openvino/utils.h"
#include "input_model.h"
#include "pass/mark_decompression_convert_constant_folding.h"
#include "pass/mark_dequantization_subgraph.h"
#include "pass/squeeze_matmul.h"
#include "rt_info/weightless_caching_attributes.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <map>
#include <memory>
#include <openvino/core/node.hpp>
#include <openvino/core/preprocess/pre_post_process.hpp>
#include <openvino/core/shape.hpp>
#include <openvino/core/type/element_type.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
@@ -35,6 +40,7 @@
#include <openvino/op/unsqueeze.hpp>
#include <openvino/pass/constant_folding.hpp>
#include <openvino/pass/make_stateful.hpp>
#include <sstream>
namespace ov {
namespace frontend {
@@ -44,6 +50,28 @@ using namespace ov::op;
namespace {
std::shared_ptr<ov::op::v0::Parameter> create_parameter(const std::string & name,
const ModelInputInfo & input_info) {
auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
param_node->set_friendly_name(name);
param_node->output(0).get_tensor().set_names({name});
return param_node;
}
std::shared_ptr<ov::Node> create_extra_input(const std::string & name, const ModelExtraInputInfo & input_info) {
if (input_info.is_parameter) {
auto param_node = std::make_shared<ov::op::v0::Parameter>(input_info.type, input_info.shape);
param_node->set_friendly_name(name);
param_node->output(0).get_tensor().set_names({name});
return param_node;
}
auto constant = std::make_shared<ov::op::v0::Constant>(input_info.type, input_info.shape,
std::vector<int64_t>{input_info.value});
constant->set_friendly_name(name);
return constant;
}
ov::pass::MakeStateful::ParamResPairs get_kv_param_res_pairs(
const std::shared_ptr<ov::Model> & model,
const std::map<std::string, std::string> & kv_param_res_names) {
@@ -177,33 +205,34 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo
std::shared_ptr<GgmlDecoder> ggml_model_decoder = ggml_model->get_model_decoder();
for (const auto & it : ggml_model_decoder->get_model_inputs()) {
params.push_back(std::dynamic_pointer_cast<ov::op::v0::Parameter>(it.second));
(*tensor_map)[it.first] = it.second;
auto param_node = create_parameter(it.first, it.second);
params.push_back(param_node);
(*tensor_map)[it.first] = param_node;
}
for (const auto & it : ggml_model_decoder->get_model_extra_inputs()) {
if (std::dynamic_pointer_cast<ov::op::v0::Parameter>(it.second)) {
params.push_back(std::dynamic_pointer_cast<ov::op::v0::Parameter>(it.second));
auto input_node = create_extra_input(it.first, it.second);
if (it.second.is_parameter) {
params.push_back(std::dynamic_pointer_cast<ov::op::v0::Parameter>(input_node));
}
(*tensor_map)[it.first] = it.second;
(*tensor_map)[it.first] = input_node;
}
for (const auto & it : ggml_model_decoder->get_model_weights()) {
(*tensor_map)[it.first] = it.second;
}
auto node_visitor = [&](std::shared_ptr<GgmlDecoder> decoder, int node_idx) {
auto translate_node = [&](const std::shared_ptr<GgmlDecoder> & decoder, int node_idx) {
auto operation_type = decoder->get_op_type(node_idx);
if (operation_type == "GGML_OP_NONE") {
return;
return ov::OutputVector{};
}
ov::OutputVector converted_outputs;
auto it = m_translator_map.find(operation_type);
FRONT_END_OP_CONVERSION_CHECK(it != m_translator_map.end(), "Translation for operation type ", operation_type,
" is not implemented.");
NodeContext node_context(decoder, tensor_map, node_idx, this);
converted_outputs = it->second(node_context);
ov::OutputVector converted_outputs = it->second(node_context);
const auto & node_output_names = decoder->get_output_names(node_idx);
FRONT_END_OP_CONVERSION_CHECK(node_output_names.size() == converted_outputs.size(), "Number of ",
@@ -216,6 +245,46 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo
(*tensor_map)[output_name] = converted_outputs[i];
}
}
return converted_outputs;
};
// To handle cases like this
// 3: [ 18432, 1, 1, 1] RESHAPE cache_r_l0 (reshaped)#3
// [ 18432, 1, 1, 1] 0: NONE cache_r_l0
// 4: [ 0, 1, 1, 1] VIEW cache_r_l0 (reshaped) (view)#4
// [ 18432, 1, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)#3
// 5: [ 0, 1, 1, 1] SCALE cache_r_l0 (reshaped) (view) (view)#5
// [ 0, 1, 1, 1] 0: VIEW cache_r_l0 (reshaped) (view)#4
// 6: [ 1, 1, 1, 1] VIEW (view)#6
// [ 1, 1, 1, 1] 0: NONE leaf_5
// 7: [ 18432, 1, 1, 1] GET_ROWS conv_states-0#7
// [ 18432, 1, 1, 1] 0: RESHAPE cache_r_l0 (reshaped)#3
// [ 1, 1, 1, 1] 1: VIEW (view)#6
// The scale is in-place which modifies cache_r_l0 (reshaped)#3
// The translation of scale overwrites cache_r in the tensor_map,
// but we also need to overwrite the old cache_r_l0 (reshaped)#3
auto refresh_inplace_aliases = [&](const std::shared_ptr<GgmlDecoder> & decoder, int inplace_node_idx,
const std::string & view_src_name) {
for (int node_idx = 0; node_idx < inplace_node_idx; node_idx++) {
if (decoder->is_view_like_alias_of(node_idx, view_src_name)) {
translate_node(decoder, node_idx);
}
}
};
auto node_visitor = [&](std::shared_ptr<GgmlDecoder> decoder, int node_idx) {
auto converted_outputs = translate_node(decoder, node_idx);
if (converted_outputs.empty()) {
return;
}
const auto inplace_src = decoder->get_inplace_op_src(node_idx);
if (inplace_src.empty()) {
return;
}
if (converted_outputs[0].get_node_shared_ptr() != nullptr) {
(*tensor_map)[inplace_src] = converted_outputs[0];
}
refresh_inplace_aliases(decoder, node_idx, inplace_src);
};
if (!m_naive) {
@@ -231,6 +300,46 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo
results.push_back(result);
}
// Debug-only hook: GGML_OPENVINO_DEBUG_NODE=<name1>,<name2>,... adds extra
// Result nodes for arbitrary intermediate tensors (looked up by name in
// tensor_map), on top of the real model outputs above. These debug
// Results are deliberately NOT added to ggml_decoder's model outputs, so
// the caller (ov_graph_compute_dynamic in utils.cpp) will not bind them
// to any ggml tensor buffer -- OpenVINO allocates its own tensor for
// them. This avoids the risk of reading a ggml buffer that has since
// been overwritten by a later in-place op (ggml aggressively reuses
// buffers), which can happen if trying to inspect an intermediate value
// via GGML_OPENVINO_DEBUG_OUTPUT by hacking it into a real output.
//
// tensor_map keys are usually the plain ggml tensor name (e.g. "embd"),
// but tensors that are recomputed multiple times in the same cgraph
// (GGML_TENSOR_FLAG_COMPUTE) are disambiguated with a "#<hash>" suffix
// (e.g. "cache_k_l0#4853", see get_tensor_ov_name()) which is not
// predictable ahead of time. To keep the env var usable, a requested
// name is matched either exactly, or as the "name" part before "#" of a
// suffixed key (first match wins; ambiguous requests should include the
// full "name#hash" form seen in a previous run's log/dump).
if (const char * debug_nodes = ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
std::stringstream ss(debug_nodes);
std::string name;
while (std::getline(ss, name, ',')) {
auto it = tensor_map->find(name);
if (it == tensor_map->end()) {
it = std::find_if(tensor_map->begin(), tensor_map->end(), [&](const auto & entry) {
return entry.first.compare(0, name.size(), name) == 0 && entry.first.size() > name.size() &&
entry.first[name.size()] == '#';
});
}
if (it == tensor_map->end()) {
GGML_LOG_WARN("GGML_OPENVINO_DEBUG_NODE: node '%s' not found in tensor map, skipping\n", name.c_str());
continue;
}
auto result = std::make_shared<v0::Result>(it->second);
result->set_friendly_name("__debug_" + it->first);
results.push_back(result);
}
}
ov::ParameterVector used_params;
for (const auto & param : params) {
if (!param->output(0).get_target_inputs().empty()) {
@@ -257,10 +366,13 @@ std::shared_ptr<Model> TranslateSession::translate_graph(const frontend::InputMo
//
// Small constants (< 16 elements) are excluded since they may be introduced by
// optimization patterns and the overhead is negligible.
//
// Note: use shape_size() rather than byte_size()/element_type().size() - GatherMatmul's default
// bias is a Constant(element::dynamic, Shape{0}), whose element_type().size() is 0 and would
// divide by zero.
size_t offset = 0;
for (auto & node : resulting_model->get_ordered_ops()) {
if (auto cnst = ov::as_type_ptr<ov::op::v0::Constant>(node);
cnst && cnst->get_byte_size() / cnst->get_element_type().size() >= 16) {
if (auto cnst = ov::as_type_ptr<ov::op::v0::Constant>(node); cnst && ov::shape_size(cnst->get_shape()) >= 16) {
auto & rt_info = cnst->get_rt_info();
if (rt_info.find(ov::WeightlessCacheAttribute::get_type_info_static()) == rt_info.end()) {
rt_info[ov::WeightlessCacheAttribute::get_type_info_static()] =
@@ -277,6 +389,12 @@ std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<M
ov::pass::Manager manager;
manager.set_per_pass_validation(true);
manager.register_pass<ov::pass::MarkCompressedFloatConstants>();
// Marks the Convert/Subtract/Multiply nodes of our GatherMatmul dequantization chain
// (make_int4_weights/make_int8_weights, for_gather_matmul=true) with disable_constant_folding,
// so it survives ConstantFolding regardless of whether the target plugin's own
// is_decompression_multiply() recognizes GatherMatmul as a valid consumer.
manager.register_pass<ov::pass::MarkDequantization>(
std::vector<ov::element::Type>{ov::element::u8, ov::element::i8, ov::element::u4, ov::element::i4});
if (ggml_model_decoder->is_stateful()) {
const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names();
@@ -289,21 +407,11 @@ std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<M
}
manager.run_passes(model);
if (ggml_model_decoder->is_stateful()) {
auto output_names = ggml_model_decoder->get_model_output_names();
std::map<std::string, int> model_output_indexes;
for (size_t i = 0; i < output_names.size(); i++) {
model_output_indexes.insert(std::make_pair(output_names[i], i));
}
ov::preprocess::PrePostProcessor ppp(model);
for (size_t i = 0; i < model->get_output_size(); i++) {
auto output_friendly_name = model->output(i).get_node_shared_ptr()->get_friendly_name();
auto output_id = model_output_indexes[output_friendly_name];
auto model_output_shape = model->output(i).get_partial_shape();
auto decoder_output_shape = ggml_model_decoder->get_output_shape(output_id);
if (model_output_shape.rank().is_static() && decoder_output_shape.rank().is_static() &&
model_output_shape.rank().get_length() + 1 == decoder_output_shape.rank().get_length() &&
decoder_output_shape[0].is_static() && decoder_output_shape[0].get_length() == 1) {
ppp.output(i).postprocess().custom([](const ov::Output<ov::Node> & node) {
if (model_output_shape.rank().is_static() && model_output_shape.rank().get_length() == 3) {
ppp.output(i).postprocess().custom([](const ov::Output<ov::Node>& node) {
auto axes = ov::op::v0::Constant::create(ov::element::i32, ov::Shape{1}, {0});
return std::make_shared<ov::op::v0::Unsqueeze>(node, axes);
});
+97 -14
View File
@@ -17,6 +17,7 @@
#include <openvino/op/reshape.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/sin.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/split.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
@@ -195,7 +196,24 @@ std::pair<ov::Output<Node>, ov::Output<Node>> make_sin_cos(int32_t * rope_params
std::make_shared<ov::op::v0::Constant>(ov::element::f32, ov::Shape{1, 1, 1, factor.size()}, factor);
}
if (rope_freqs_weight) {
freq_factors = std::make_shared<ov::op::v1::Divide>(freq_factors, rope_freqs_weight);
Output<Node> rope_factors = std::make_shared<ov::op::v8::Slice>(
rope_freqs_weight,
ov::op::v0::Constant::create(ov::element::i64, {1}, {0}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) n_dims_half}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {1}),
ov::op::v0::Constant::create(ov::element::i64, {1}, {rope_freqs_weight->get_output_partial_shape(0).rank().get_length() - 1}));
if (stateful) {
rope_factors = std::make_shared<ov::op::v1::Reshape>(
rope_factors,
ov::op::v0::Constant::create(ov::element::i64, {3}, {(int64_t) 1, (int64_t) 1, (int64_t) n_dims_half}),
false);
} else {
rope_factors = std::make_shared<ov::op::v1::Reshape>(
rope_factors,
ov::op::v0::Constant::create(ov::element::i64, {4}, {(int64_t) 1, (int64_t) 1, (int64_t) 1, (int64_t) n_dims_half}),
false);
}
freq_factors = std::make_shared<ov::op::v1::Divide>(freq_factors, rope_factors);
}
auto theta_extrap = std::make_shared<ov::op::v1::Multiply>(freq_factors, inp_pos);
@@ -234,23 +252,30 @@ std::pair<ov::Output<Node>, ov::Output<Node>> make_sin_cos(int32_t * rope_params
return std::make_pair(sin_theta, cos_theta);
}
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len) {
// Only works for VIEW operations that slice at the lowest dimension
// If the VIEW also reshape the result, `slice_len` should be provided
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len, int axis) {
// Only works for VIEW operations that does a non-strided slice with optinal reshape on the slice result.
// The function only does the slice part, the reshape (if any) should be handled by the caller.
// Default axis is -1, which means slicing the last dimension.
// If the VIEW reshapes the result, `slice_len` should be provided
auto input = context.get_input(input_index);
auto * op_params = (size_t *) context.get_input_op_params(input_index);
auto src1_stride = context.get_input_stride(input_index);
auto src_stride = context.get_input_stride(input_index);
int64_t split_addr = op_params[0] / src1_stride[3];
int64_t slice_start = op_params[0] / src_stride[3];
if (slice_len == 0) {
slice_len = context.get_input_shape(input_index)[3].get_length();
}
int64_t slice_end = split_addr + slice_len;
int64_t slice_end = slice_start + slice_len;
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {split_addr});
auto begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_start});
auto end = ov::op::v0::Constant::create(ov::element::i64, {1}, {slice_end});
auto stride = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {context.is_stateful() ? 2 : 3});
ov::Output<ov::Node> axes;
if (axis == -1) {
axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {context.is_stateful() ? 2 : 3});
} else {
axes = ov::op::v0::Constant::create(ov::element::i64, {1}, {axis});
}
auto sliced = std::make_shared<ov::op::v8::Slice>(input, begin, end, stride, axes);
return sliced;
}
@@ -267,17 +292,40 @@ ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int inp
// If translate_view already resolved this VIEW (produced a Slice), the input
// will already have the expected shape — skip re-slicing.
//
// Two notions of "matches" are accepted per axis:
// - both dims static and equal, OR
// - both dims dynamic.
// The dynamic case matters for the MoE expert-plane views: translate_view now emits a
// DYNAMIC-token slice (so the token dim is not frozen). An all-static-only check would
// see the dynamic token dim, decide the shapes "don't match", and fall through to
// re-slice/flatten the already-resolved view (a Reshape to the full flattened
// n_expert_used*n_embd tail, which then conflicts with the single-plane input). Treat a
// dynamic-vs-dynamic axis as matching so the already-resolved view is reused as-is.
//
// A third case matters for split-model MoE fragments: translate_view resolves the
// expert-plane view against the fragment's INPUT parameter. When the graph is split
// the token axis of that parameter may already be concrete (static n_tokens) even
// though get_view_input_ov_shape() still reports it as dynamic (-1). The resolved
// view is then static [1,1,n_tokens,n_embd] while `expected` is [1,1,?,n_embd].
// An "expected dynamic, actual static" axis is a valid concretization of the SAME
// resolved view, so treat it as matching too. Falling through to process_single_view
// here would re-slice/re-flatten the already-resolved single-plane view against the
// recorded (multi-plane) source strides and emit a constant-target Reshape whose baked
// dims no longer divide the concretized input -> "dimensions do not evenly divide".
auto expected_ov_shape = context.get_view_input_ov_shape(input_index, 0);
auto actual_shape = input.get_partial_shape();
if (expected_ov_shape.rank().is_static() && actual_shape.rank().is_static() &&
expected_ov_shape.rank() == actual_shape.rank()) {
bool shapes_match = true;
for (int64_t i = 0; i < expected_ov_shape.rank().get_length(); ++i) {
if (!expected_ov_shape[i].is_static() || !actual_shape[i].is_static()) {
shapes_match = false;
break;
}
if (expected_ov_shape[i] != actual_shape[i]) {
const bool both_dynamic = expected_ov_shape[i].is_dynamic() && actual_shape[i].is_dynamic();
const bool both_static_equal = expected_ov_shape[i].is_static() && actual_shape[i].is_static() &&
expected_ov_shape[i] == actual_shape[i];
// expected dynamic, actual static: the resolved view already carries the
// concrete size for this fragment; reuse it rather than re-materializing.
const bool expected_dyn_actual_static = expected_ov_shape[i].is_dynamic() && actual_shape[i].is_static();
if (!both_dynamic && !both_static_equal && !expected_dyn_actual_static) {
shapes_match = false;
break;
}
@@ -758,6 +806,41 @@ ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int inp
return current;
};
// Special case: ggml collapses VIEW-of-VIEW chains so that `view_offs` is always an
// ABSOLUTE offset from the true root allocation, regardless of how many VIEW levels
// are in between (see ggml_new_tensor_impl). `src[0]` is still the immediate op-graph
// parent though, which can be a DIFFERENT (already narrowed) VIEW with the SAME ggml
// shape as this one but a different absolute offset -- e.g. a per-layer deepstack
// slice `view_2d(embd, n_embd, n_tokens, embd->nb[1], layer*n_embd*sizeof(float))`
// whose src[0] ("embd") is itself already a zero-offset VIEW of the true root (the
// padded embedding). Chaining through "embd" here would try to re-slice an already
// 2-narrowed tensor using a root-relative offset, going out of bounds and silently
// falling back to a no-op (returning the wrong, already-resolved sibling slice).
// Detect this (same shape as the immediate src, but different absolute offset) and
// re-slice directly from the untouched root using the innermost view's absolute
// offset against the ROOT's own shape/stride instead of chaining through src[0].
{
auto innermost_offset = context.get_view_input_offset(input_index, 0);
auto innermost_src_offset = context.get_view_input_src_offset(input_index, 0);
auto innermost_shape = context.get_view_input_ggml_shape(input_index, 0);
auto innermost_src_shape = context.get_view_input_src_ggml_shape(input_index, 0);
if (innermost_offset != innermost_src_offset && innermost_shape == innermost_src_shape) {
size_t root_view_idx = view_input_size - 1;
auto root_ggml_shape = context.get_view_input_src_ggml_shape(input_index, root_view_idx);
auto root_stride = context.get_view_input_src_stride(input_index, root_view_idx);
auto root_offset = context.get_view_input_src_offset(input_index, root_view_idx);
auto root_ov_shape = context.get_view_input_src_ov_shape(input_index, root_view_idx);
auto root_name = context.get_view_input_src_name(input_index, root_view_idx);
auto innermost_stride = context.get_view_input_stride(input_index, 0);
auto innermost_ov_shape = context.get_view_input_ov_shape(input_index, 0);
auto innermost_name = context.get_view_input_name(input_index, 0);
return process_single_view(input, innermost_offset, innermost_stride, innermost_shape, innermost_ov_shape,
innermost_name, root_offset, root_stride, root_ggml_shape, root_ov_shape,
root_name);
}
}
// Process views from the base tensor (last) to the current view (first)
// Start with the base tensor
ov::Output<ov::Node> current = input;
+1 -1
View File
@@ -62,7 +62,7 @@ std::pair<ov::Output<Node>, ov::Output<Node>> make_sin_cos(int32_t * rope_params
bool imrope = false,
bool stateful = false);
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len = 0);
ov::Output<ov::Node> process_view_input(const NodeContext & context, int input_index, int slice_len = 0, int axis = -1);
ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int input_index);
+308 -56
View File
@@ -4,6 +4,7 @@
#include "ggml-openvino-extra.h"
#include "ggml-openvino/ggml-decoder.h"
#include "ggml.h"
#include "model-cache.h"
#include "openvino/frontend.h"
#include "openvino/input_model.h"
@@ -134,6 +135,20 @@ static std::optional<ov::Tensor> try_make_kv_sliced_tensor(std::shared_ptr<GgmlO
return ov::Tensor(ggml_decoder->get_ov_type(ggml_tensor), sliced_shape, ggml_tensor->data);
}
static uint64_t ggml_openvino_model_cache_extra_cfg(const std::string & device, bool stateful) {
const char * manual_gqa_env = ggml_openvino_getenv_str("GGML_OPENVINO_MANUAL_GQA_ATTN");
const bool manual_gqa_enabled = manual_gqa_env != nullptr ?
ggml_openvino_getenv_int("GGML_OPENVINO_MANUAL_GQA_ATTN") > 0 :
device == "GPU";
uint64_t extra_cfg = 0;
extra_cfg = extra_cfg * 131 + (stateful ? 1u : 0u);
extra_cfg = extra_cfg * 131 + (ggml_openvino_reduce_compile_mem_enabled() ? 1u : 0u);
extra_cfg = extra_cfg * 131 + (ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_SLICE") ? 1u : 0u);
extra_cfg = extra_cfg * 131 + (manual_gqa_enabled ? 1u : 0u);
return extra_cfg;
}
ov::Tensor create_ov_output_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder,
std::shared_ptr<ov::InferRequest> infer_request,
int output_index,
@@ -170,8 +185,24 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
const auto & stateful = r_ctx->stateful;
static auto is_static = false;
static const bool cache_disabled = ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_CACHE");
// is_model_splitted is O(n_nodes^2) plus a create_weight_nodes scan and takes ~20 ms
// on a Llama-1B decode graph. It is called once per graph_compute invocation but the
// graph shape is identical across all decode steps, so memoize by graph_key: compute
// graph_key first (a few hundred us), and if the same key is already in decoder_cache
// we know the graph is not splitted (only not-splitted graphs get inserted there).
graph_key key(cgraph);
bool key_seen = false;
if (!cache_disabled) {
std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex);
key_seen = r_ctx->decoder_cache.find(key) != r_ctx->decoder_cache.end();
}
bool model_is_splitted = key_seen ? false : is_model_splitted(cgraph);
if (is_naive(cgraph)) {
if (!is_model_splitted(cgraph)) {
if (!model_is_splitted) {
return naive_compute(cgraph, core, device, config);
}
}
@@ -184,8 +215,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
ComputeParams c_params;
std::tie(m_params, c_params) = GgmlOvDecoder::compute_llm_params(cgraph, is_static);
graph_key key(cgraph);
static const bool cache_enabled = !ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_CACHE");
const bool cache_enabled = !model_is_splitted && !cache_disabled;
bool cache_hit = false;
int64_t decoder_end_time;
@@ -205,6 +235,7 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
if (cache_hit) {
entry = it->second;
} else {
r_ctx->clear_caches_locked();
auto mutex = std::make_shared<std::mutex>();
entry = std::make_shared<decoder_runtime_ctx>(mutex);
r_ctx->decoder_cache[key] = entry;
@@ -286,48 +317,171 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
conversion_end_time = decoder_end_time;
compile_end_time = decoder_end_time;
} else {
// Fail fast: a cache-miss recompile feeds weight data to compile_model, but
// GGML_OPENVINO_RELEASE_WEIGHTS (or GGML_OPENVINO_MEMORY_OPTIMIZE on GPU)
// may have already dropped the host weight pages
// (they would read as zeros). That mode requires stable graph shapes.
if (ggml_openvino_weight_buffers_released()) {
GGML_ABORT(
"ggml-openvino: a new graph needs to be compiled but host weight buffers were already "
"released via GGML_OPENVINO_RELEASE_WEIGHTS/GGML_OPENVINO_MEMORY_OPTIMIZE. This mode requires "
"stable graph shapes; disable host weight release for dynamic workloads.");
}
if (cache_enabled) {
std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex);
r_ctx->infer_request_cache.erase(key);
}
bool model_is_splitted = is_model_splitted(cgraph);
// Frontend-level compiled-model cache (GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR): if this model
// was compiled before, import the saved blob and skip requant + convert +
// compile. Only the dynamic single-model path is cached (split models compile
// two graphs and are left to the plugin-level ov::cache_dir). The decoder is
// still needed for I/O mapping, but can be built without weight nodes since
// the weights are baked into the imported CompiledModel.
const std::string model_cache_dir = ggml_openvino_model_cache_dir();
uint64_t model_fp = 0;
std::string blob_path, manifest_path;
bool imported = false;
// When the frontend model cache is active it supersedes the plugin-level
// ov::cache_dir: a blob exported from a model compiled WITH cache_dir cannot
// be re-imported (import returns an uninitialized model). Strip cache_dir /
// cache_mode from the config used for the cached compile and the import.
ov::AnyMap mc_config = config;
if (!model_cache_dir.empty()) {
mc_config.erase("CACHE_DIR");
mc_config.erase("CACHE_MODE");
}
if (!model_cache_dir.empty() && !model_is_splitted) {
const uint64_t extra_cfg = ggml_openvino_model_cache_extra_cfg(device, stateful);
model_fp = ggml_openvino_model_fingerprint(cgraph, device, /*fa=*/true, m_params.rope_params,
15, extra_cfg);
blob_path = ggml_openvino_model_cache_blob_path(model_cache_dir, model_fp);
manifest_path = ggml_openvino_model_cache_manifest_path(model_cache_dir, model_fp);
std::ifstream blob_in(blob_path, std::ios::binary);
bool blob_ok = blob_in.is_open();
bool manifest_ok = blob_ok && ggml_openvino_model_cache_verify_manifest(manifest_path, cgraph, model_fp);
if (blob_ok && manifest_ok) {
int64_t import_start = ggml_time_us();
try {
ov::CompiledModel cm;
auto remote_context = ggml_openvino_get_remote_context();
if (remote_context.has_value()) {
cm = core.import_model(blob_in, remote_context.value(), mc_config);
} else {
cm = core.import_model(blob_in, device, mc_config);
}
// Lightweight decoder: names-only weight map (membership is all the
// decoder needs; weights live in the imported model).
std::map<std::string, std::shared_ptr<ov::Node>> weight_names;
for (const auto & n : GgmlOvDecoder::collect_weight_names(cgraph)) {
weight_names[n] = nullptr;
}
ggml_decoder = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, weight_names,
is_static, stateful, model_is_splitted);
infer_request = std::make_shared<ov::InferRequest>(cm.create_infer_request());
entry->ptr = ggml_decoder;
// Names must match the decoder's ggml-tensor keys. The non-cached
// path keys off Parameter/Result *friendly names* (set by the
// frontend); export_model preserves these, and each compiled-model
// port's node is exactly that Parameter/Result. Use the port nodes
// directly (NOT get_runtime_model(), whose graph differs and is
// unsafe to deref this way).
for (const auto & p : cm.inputs()) {
ov_input_names.push_back(p.get_node()->get_friendly_name());
}
for (const auto & o : cm.outputs()) {
ov_output_names.push_back(o.get_node()->get_friendly_name());
}
imported = true;
if (ggml_openvino_getenv_int("GGML_OPENVINO_PROFILING")) {
GGML_LOG_INFO(" - Model cache import time: %.3f ms \n",
(ggml_time_us() - import_start) / 1000.0);
}
GGML_LOG_INFO("ggml-openvino: model cache HIT %s\n", blob_path.c_str());
} catch (const std::exception & e) {
GGML_LOG_WARN("ggml-openvino: model cache import failed (%s), recompiling\n", e.what());
imported = false;
}
}
}
std::shared_ptr<ov::Model> model;
auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph);
ggml_decoder = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, model_weights, is_static,
stateful, model_is_splitted);
decoder_end_time = ggml_time_us();
auto input_model = std::make_shared<ov::frontend::ggml::InputModel>(ggml_decoder);
model = ov::frontend::ggml::FrontEnd::convert(input_model);
ggml_decoder->clear_model_weights();
conversion_end_time = ggml_time_us();
if (ggml_openvino_getenv_int("GGML_OPENVINO_DUMP_IR")) {
char timestamped_filename[64];
auto timestamp = (long long) ggml_time_us();
snprintf(timestamped_filename, sizeof(timestamped_filename), "model_%lld.xml", timestamp);
ov::serialize(model, timestamped_filename);
}
ov::CompiledModel compiled_model;
auto remote_context = ggml_openvino_get_remote_context();
if (remote_context.has_value()) {
compiled_model = core.compile_model(model, remote_context.value(), config);
if (imported) {
decoder_end_time = conversion_end_time = compile_end_time = ggml_time_us();
} else {
compiled_model = core.compile_model(model, device, config);
}
compile_end_time = ggml_time_us();
infer_request = std::make_shared<ov::InferRequest>(compiled_model.create_infer_request());
entry->ptr = ggml_decoder;
auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph);
for (const auto & ov_param : model->get_parameters()) {
ov_input_names.push_back(ov_param->get_friendly_name());
}
for (const auto & ov_output : model->get_results()) {
ov_output_names.push_back(ov_output->get_friendly_name());
}
ggml_decoder = std::make_shared<GgmlOvDecoder>(cgraph, m_params, c_params, model_weights, is_static,
stateful, model_is_splitted);
decoder_end_time = ggml_time_us();
auto input_model = std::make_shared<ov::frontend::ggml::InputModel>(ggml_decoder);
model = ov::frontend::ggml::FrontEnd::convert(input_model);
ggml_decoder->clear_model_weights();
conversion_end_time = ggml_time_us();
if (ggml_openvino_getenv_int("GGML_OPENVINO_DUMP_IR")) {
char timestamped_filename[64];
auto timestamp = (long long) ggml_time_us();
snprintf(timestamped_filename, sizeof(timestamped_filename), "model_%lld.xml", timestamp);
ov::serialize(model, timestamped_filename);
}
// Use the cache-stripped config when the frontend model cache is active, so
// the resulting CompiledModel can be exported and later re-imported.
const ov::AnyMap & compile_config = model_cache_dir.empty() ? config : mc_config;
ov::CompiledModel compiled_model;
auto remote_context = ggml_openvino_get_remote_context();
if (remote_context.has_value()) {
compiled_model = core.compile_model(model, remote_context.value(), compile_config);
} else {
compiled_model = core.compile_model(model, device, compile_config);
}
compile_end_time = ggml_time_us();
// Export to the frontend model cache for next time. Publish the blob first,
// then the manifest, so a cache hit only sees fully written artifacts.
if (!model_cache_dir.empty() && !model_is_splitted && model_fp != 0) {
try {
const std::string blob_tmp = blob_path + ".tmp";
const std::string manifest_tmp = manifest_path + ".tmp";
if (ggml_openvino_model_cache_write_manifest(manifest_tmp, cgraph, model_fp)) {
std::ofstream blob_out(blob_tmp, std::ios::binary | std::ios::trunc);
if (blob_out.is_open()) {
compiled_model.export_model(blob_out);
blob_out.close();
if (blob_out.good()) {
if (std::rename(blob_tmp.c_str(), blob_path.c_str()) == 0 &&
std::rename(manifest_tmp.c_str(), manifest_path.c_str()) == 0) {
GGML_LOG_INFO("ggml-openvino: model cache WROTE %s\n", blob_path.c_str());
} else {
std::remove(blob_tmp.c_str());
std::remove(manifest_tmp.c_str());
}
} else {
std::remove(blob_tmp.c_str());
std::remove(manifest_tmp.c_str());
}
} else {
std::remove(manifest_tmp.c_str());
}
}
} catch (const std::exception & e) {
GGML_LOG_WARN("ggml-openvino: model cache export failed: %s\n", e.what());
}
}
infer_request = std::make_shared<ov::InferRequest>(compiled_model.create_infer_request());
entry->ptr = ggml_decoder;
for (const auto & ov_param : model->get_parameters()) {
ov_input_names.push_back(ov_param->get_friendly_name());
}
for (const auto & ov_output : model->get_results()) {
ov_output_names.push_back(ov_output->get_friendly_name());
}
} // end non-imported (compile) path
if (cache_enabled) {
std::lock_guard<std::mutex> map_lock(r_ctx->ctx_mutex);
@@ -358,7 +512,17 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
}
for (size_t i = 0; i < ov_output_names.size(); i++) {
auto * ggml_tensor = ggml_decoder->get_model_outputs().at(ov_output_names[i]);
// Debug-only outputs added via GGML_OPENVINO_DEBUG_NODE (see
// translate_session.cpp) have no corresponding ggml tensor; leave
// them unbound so OpenVINO allocates its own tensor for them,
// rather than aliasing a ggml buffer that may be overwritten by a
// later in-place op before we get to read it.
const auto & model_outputs = ggml_decoder->get_model_outputs();
auto model_output_it = model_outputs.find(ov_output_names[i]);
if (model_output_it == model_outputs.end()) {
continue;
}
auto * ggml_tensor = model_output_it->second;
if (ggml_nbytes(ggml_tensor) == 0) {
continue;
}
@@ -370,7 +534,8 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
infer_request->infer();
infer_end_time = ggml_time_us();
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT")) {
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT") ||
ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
for (size_t i = 0; i < ov_output_names.size(); i++) {
const auto output_tensor = infer_request->get_output_tensor(i);
print_output_tensor_info(ov_output_names[i], output_tensor, output_tensor.data());
@@ -390,6 +555,20 @@ enum ggml_status ov_graph_compute_dynamic(ggml_cgraph * cgraph, std::shared_ptr<
}
}
// GGML_OPENVINO_RELEASE_WEIGHTS (or GGML_OPENVINO_MEMORY_OPTIMIZE on GPU): the plugin holds its own device copy of
// every weight after compile, so the host weight buffers can be dropped to reclaim
// RSS. The GPU backend uses a single dynamic-shape model for both prefill and decode,
// so once a graph is compiled it is reused for the whole session — the only thing
// that forces a recompile is clear_caches() on backend teardown. We therefore release
// on the first cache-hit (model compiled, plugin has its copy) and, crucially, pin the
// compiled-model cache so it survives backend teardown (see ggml_backend_openvino_free).
// Without the pin, a later test/context would recompile against the now-dropped pages.
// A genuinely new graph still fails fast at the cache-miss compile branch.
if (cache_hit && ggml_openvino_release_weights_enabled(device) &&
!ggml_openvino_weight_buffers_released()) {
ggml_openvino_release_weight_buffers();
}
return GGML_STATUS_SUCCESS;
}
@@ -446,6 +625,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
if (cache_hit) {
entry = it->second;
} else {
r_ctx->clear_caches_locked();
auto mutex = std::make_shared<std::mutex>();
entry = std::make_shared<decoder_runtime_ctx>(mutex);
r_ctx->decoder_cache[key] = entry;
@@ -576,7 +756,12 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
}
for (size_t i = 0; i < ov_output_names_local.size(); i++) {
auto * ggml_tensor = ggml_decoder->get_model_outputs().at(ov_output_names_local[i]);
const auto & model_outputs = ggml_decoder->get_model_outputs();
auto model_output_it = model_outputs.find(ov_output_names_local[i]);
if (model_output_it == model_outputs.end()) {
continue;
}
auto * ggml_tensor = model_output_it->second;
auto output_tensor = create_ov_output_tensor(ggml_decoder, infer_request, i, ggml_tensor);
infer_request->set_output_tensor(i, output_tensor);
}
@@ -585,7 +770,8 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
infer_request->infer();
ov_raw_infer_total += ggml_time_us() - ov_raw_infer_start;
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT")) {
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT") ||
ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
for (size_t i = 0; i < ov_output_names_local.size(); i++) {
const auto output_tensor = infer_request->get_output_tensor(i);
print_output_tensor_info(ov_output_names_local[i], output_tensor, output_tensor.data());
@@ -606,7 +792,12 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
}
for (size_t i = 0; i < ov_output_names_local.size(); i++) {
auto * ggml_tensor = ggml_decoder->get_model_outputs().at(ov_output_names_local[i]);
const auto & model_outputs = ggml_decoder->get_model_outputs();
auto model_output_it = model_outputs.find(ov_output_names_local[i]);
if (model_output_it == model_outputs.end()) {
continue;
}
auto * ggml_tensor = model_output_it->second;
auto output_tensor = create_ov_output_tensor(ggml_decoder, infer_request, i, ggml_tensor);
infer_request->set_output_tensor(i, output_tensor);
}
@@ -616,7 +807,8 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
infer_end_time = ggml_time_us();
ov_raw_infer_total = infer_end_time - ov_raw_infer_start;
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT")) {
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT") ||
ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
for (size_t i = 0; i < ov_output_names_local.size(); i++) {
const auto output_tensor = infer_request->get_output_tensor(i);
print_output_tensor_info(ov_output_names_local[i], output_tensor, output_tensor.data());
@@ -642,6 +834,18 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
// Step 1 compares each node's recorded use_count with actual fan-out references in node->src.
// Step 2 verifies that node inputs come from model nodes/weights/leafs; external sources imply split.
bool is_model_splitted(ggml_cgraph * cgraph) {
static const bool fallback_enabled = ggml_openvino_getenv_int("GGML_OPENVINO_ENABLE_FALLBACK") != 0;
if (!fallback_enabled) {
return false;
}
// Backend op tests execute each node through ggml_graph_view(), which preserves the original
// graph use_counts while exposing only one node. Treat those single-node views as regular
// naive graphs so intermediate ops do not look like split-model fragments.
if (cgraph->n_nodes <= 1 && cgraph->n_leafs == 0) {
return false;
}
// check the nodes of the model are used by the following nodes, through compare the node's use count and the count of nodes that use it as input. If does not match, return true, else return false.
for (int i = 0; i < cgraph->n_nodes; i++) {
ggml_tensor * node = cgraph->nodes[i];
@@ -670,7 +874,17 @@ bool is_model_splitted(ggml_cgraph * cgraph) {
}
}
// if all nodes's src node's src is not come from the nodes in the model, we think the model is splitted. This is a complementary check for the above check, because for some special case like the output node is not used by any node, the use count and input use count are both 0, we can not determine whether the model is splitted or not just based on the first check.
auto model_weights = GgmlOvDecoder::create_weight_nodes(cgraph, true);
// Only weight-name membership is needed below. With GGML_OPENVINO_REDUCE_COMPILE_MEM
// use the name-only collector (no weight extraction); otherwise keep the original
// behavior of building (naive) weight nodes and take their names.
std::set<std::string> model_weights;
if (ggml_openvino_reduce_compile_mem_enabled()) {
model_weights = GgmlOvDecoder::collect_weight_names(cgraph);
} else {
for (const auto & kv : GgmlOvDecoder::create_weight_nodes(cgraph, true)) {
model_weights.insert(kv.first);
}
}
std::set<ggml_tensor *> model_nodes(cgraph->nodes, cgraph->nodes + cgraph->n_nodes);
// leaf nodes
std::set<ggml_tensor *> model_leafs(cgraph->leafs, cgraph->leafs + cgraph->n_leafs);
@@ -752,7 +966,17 @@ enum ggml_status naive_compute(ggml_cgraph * cgraph,
auto ov_results = model->get_results();
for (size_t i = 0; i < ov_results.size(); i++) {
auto output_tensor = infer_request->get_output_tensor(i);
auto * ggml_tensor = decoder->get_model_outputs().at(ov_results[i]->get_friendly_name());
const auto & model_outputs = decoder->get_model_outputs();
auto model_output_it = model_outputs.find(ov_results[i]->get_friendly_name());
if (model_output_it == model_outputs.end()) {
// Debug-only output added via GGML_OPENVINO_DEBUG_NODE; nothing to copy into.
if (ggml_openvino_getenv_int("GGML_OPENVINO_DEBUG_OUTPUT") ||
ggml_openvino_getenv_str("GGML_OPENVINO_DEBUG_NODE")) {
print_output_tensor_info(ov_results[i]->get_friendly_name(), output_tensor, output_tensor.data());
}
continue;
}
auto * ggml_tensor = model_output_it->second;
std::memcpy(ggml_tensor->data, output_tensor.data(), output_tensor.get_byte_size());
}
return GGML_STATUS_SUCCESS;
@@ -837,8 +1061,10 @@ ov::Tensor convert_ggml_input_to_ov(std::shared_ptr<GgmlOvDecoder> ggml_decoder,
ov::Tensor get_ov_input_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder, const std::string & param_name) {
ov::Tensor input_tensor;
if (ggml_decoder->get_model_extra_inputs().find(param_name) != ggml_decoder->get_model_extra_inputs().end()) {
input_tensor = *ggml_decoder->get_model_extra_input_values().at(param_name);
auto extra_input = ggml_decoder->get_model_extra_inputs().find(param_name);
if (extra_input != ggml_decoder->get_model_extra_inputs().end()) {
input_tensor = ov::Tensor(extra_input->second.type, extra_input->second.shape);
*input_tensor.data<int64_t>() = extra_input->second.value;
} else {
input_tensor = convert_ggml_input_to_ov(ggml_decoder, param_name);
}
@@ -853,16 +1079,13 @@ ov::Tensor get_ov_input_tensor_static_decode(std::shared_ptr<GgmlOvDecoder> ggml
if (GgmlOvDecoder::is_inp_tok(ggml_tensor, op) || GgmlOvDecoder::is_inp_pos(ggml_tensor, op) ||
GgmlOvDecoder::is_kv_idx(ggml_tensor, op)) {
assert(ggml_tensor->ne[0] == 1);
ov::Shape input_shape = {1, 1, 1, 1};
// IMROPE's inp_pos holds one value per t/h/w/e plane instead of a single position;
// with a single decode token the planes are still contiguous, so a flat copy works.
const int n_planes = GgmlOvDecoder::is_inp_pos(ggml_tensor, op) ? GgmlOvDecoder::get_inp_pos_n_planes(op) : 1;
assert(ggml_tensor->ne[0] == n_planes);
ov::Shape input_shape = {1, 1, 1, (size_t) n_planes};
ov::Tensor input_tensor(ggml_decoder->get_ov_type(ggml_tensor), input_shape);
if (ggml_tensor->type == GGML_TYPE_I32) {
*input_tensor.data<int32_t>() = *((int32_t *) ggml_tensor->data);
} else if (ggml_tensor->type == GGML_TYPE_I64) {
*input_tensor.data<int64_t>() = *((int64_t *) ggml_tensor->data);
} else {
throw std::runtime_error("Unexpected tensor type for " + param_name);
}
std::memcpy(input_tensor.data(), ggml_tensor->data, n_planes * ggml_type_size(ggml_tensor->type));
return input_tensor;
}
@@ -908,6 +1131,35 @@ ov::Tensor get_ov_input_tensor_static_prefill(std::shared_ptr<GgmlOvDecoder> ggm
const size_t chunk_valid_size = std::min(chunk_size, input_len - chunk_index * chunk_size);
const size_t chunk_pad_size = chunk_size - chunk_valid_size;
if (GgmlOvDecoder::is_inp_pos(ggml_tensor, op) && GgmlOvDecoder::get_inp_pos_n_planes(op) > 1) {
// IMROPE: inp_pos stacks n_planes (t/h/w/e) position planes, each of length
// input_len; pad every plane independently so they stay aligned to chunk_size.
const int n_planes = GgmlOvDecoder::get_inp_pos_n_planes(op);
const size_t element_size = ggml_type_size(ggml_tensor->type);
ov::Shape input_shape = {1, 1, 1, (size_t) n_planes * chunk_size};
ov::Tensor input_tensor(ggml_decoder->get_ov_type(ggml_tensor), input_shape);
for (int p = 0; p < n_planes; p++) {
const char * src =
(const char *) ggml_tensor->data + (p * input_len + chunk_index * chunk_size) * element_size;
char * dst = (char *) input_tensor.data() + p * chunk_size * element_size;
std::memcpy(dst, src, chunk_valid_size * element_size);
if (chunk_pad_size > 0) {
if (ggml_tensor->type == GGML_TYPE_I32) {
int32_t last_value = *((const int32_t *) src + chunk_valid_size - 1);
int32_t * out = (int32_t *) dst;
std::fill(out + chunk_valid_size, out + chunk_size, last_value + 1);
} else if (ggml_tensor->type == GGML_TYPE_I64) {
int64_t last_value = *((const int64_t *) src + chunk_valid_size - 1);
int64_t * out = (int64_t *) dst;
std::fill(out + chunk_valid_size, out + chunk_size, last_value + 1);
} else {
throw std::runtime_error("Unexpected tensor type for " + param_name);
}
}
}
return input_tensor;
}
if (GgmlOvDecoder::is_inp_tok(ggml_tensor, op) || GgmlOvDecoder::is_inp_pos(ggml_tensor, op) ||
GgmlOvDecoder::is_kv_idx(ggml_tensor, op)) {
ov::Shape input_shape = {1, 1, 1, chunk_size};
+54 -7
View File
@@ -4,6 +4,7 @@
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <functional>
#include <memory>
#include <mutex>
#include <openvino/runtime/core.hpp>
@@ -17,28 +18,68 @@ struct graph_key {
int n_nodes;
std::string first_node_name;
std::string last_node_name;
std::vector<std::string> input_src_names;
graph_key(const ggml_cgraph * cgraph) : n_nodes(cgraph->n_nodes) {
if (n_nodes > 0) {
first_node_name = cgraph->nodes[0]->name;
last_node_name = cgraph->nodes[n_nodes - 1]->name;
}
auto get_input_key_name = [](const ggml_cgraph * graph, const ggml_tensor * tensor) {
std::string name = tensor->name;
const size_t hash_pos = ggml_hash_find(&graph->visited_hash_set, tensor);
if (((tensor->flags & GGML_TENSOR_FLAG_COMPUTE) || GgmlOvDecoder::is_kvcache(tensor, nullptr)) &&
hash_pos != GGML_HASHSET_FULL && ggml_bitset_get(graph->visited_hash_set.used, hash_pos)) {
name += "#" + std::to_string(hash_pos);
}
return name;
};
std::vector<std::string> node_names;
node_names.reserve(cgraph->n_nodes);
for (int node_idx = 0; node_idx < cgraph->n_nodes; node_idx++) {
node_names.emplace_back(cgraph->nodes[node_idx]->name);
}
for (int node_idx = 0; node_idx < cgraph->n_nodes; node_idx++) {
const ggml_tensor * node = cgraph->nodes[node_idx];
for (int src_idx = 0; src_idx < GGML_MAX_SRC; src_idx++) {
const ggml_tensor * src = node->src[src_idx];
if (src == nullptr || src->name[0] == '\0') {
continue;
}
const std::string src_name = get_input_key_name(cgraph, src);
if (std::find(node_names.begin(), node_names.end(), src_name) != node_names.end()) {
continue;
}
if (src_name.find("weight") != std::string::npos) {
continue;
}
input_src_names.push_back(std::to_string(node_idx) + ":" + std::to_string(src_idx) + ":" + src_name);
}
}
}
bool operator==(const graph_key & other) const {
return n_nodes == other.n_nodes && first_node_name == other.first_node_name &&
last_node_name == other.last_node_name;
last_node_name == other.last_node_name && input_src_names == other.input_src_names;
}
};
struct graph_key_hash {
size_t operator()(const graph_key & key) const {
size_t h = std::hash<int>{}(key.n_nodes);
size_t hash = std::hash<int>{}(key.n_nodes);
if (key.n_nodes > 0) {
h ^= std::hash<std::string>{}(key.first_node_name) + 0x9e3779b9 + (h << 6) + (h >> 2);
h ^= std::hash<std::string>{}(key.last_node_name) + 0x9e3779b9 + (h << 6) + (h >> 2);
hash ^= std::hash<std::string>{}(key.first_node_name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
hash ^= std::hash<std::string>{}(key.last_node_name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return h;
for (const auto & input_src_name : key.input_src_names) {
hash ^= std::hash<std::string>{}(input_src_name) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
};
@@ -66,13 +107,19 @@ struct ov_runtime_context {
ov_runtime_context() : device("CPU"), stateful(false), stateful_kv_size(0), backend_count(0) {}
void clear_caches() {
std::lock_guard<std::mutex> lock(ctx_mutex);
void clear_caches_locked() {
decoder_cache.clear();
infer_request_cache.clear();
infer_request_cache_prefill.clear();
ov_input_names_cache.clear();
ov_output_names_cache.clear();
kv_state_input_name_map.clear();
stateful_kv_size = 0;
}
void clear_caches() {
std::lock_guard<std::mutex> lock(ctx_mutex);
clear_caches_locked();
}
};
File diff suppressed because it is too large Load Diff
-37
View File
@@ -81,43 +81,6 @@ static __dpct_inline__ T op_elu(T x) {
return (x > static_cast<T>(0.f)) ? x : op_expm1(x);
}
template<typename T>
static __dpct_inline__ T op_tanh(T x) {
if constexpr (std::is_same_v<T, sycl::ext::oneapi::bfloat16>) {
constexpr int ver = __INTEL_LLVM_COMPILER;
#if defined(__INTEL_LLVM_COMPILER) && (__INTEL_LLVM_COMPILER >= 20260000)
return sycl::ext::oneapi::experimental::tanh(x);
#else
return static_cast<T>(sycl::tanh(static_cast<float>(x)));
#endif
} else {
return sycl::tanh(x);
}
}
template<typename T>
static __dpct_inline__ T op_gelu(T x) {
const T GELU_COEF_A = static_cast<T>(0.044715f);
const T SQRT_2_OVER_PI = static_cast<T>(0.79788456080286535587989211986876f);
return static_cast<T>(0.5f) * x *
(static_cast<T>(1.0f) +
op_tanh(SQRT_2_OVER_PI * x * (static_cast<T>(1.0f) + GELU_COEF_A * x * x)));
}
template<typename T>
static __dpct_inline__ T op_exp(T x) {
if constexpr (std::is_same_v<T, sycl::ext::oneapi::bfloat16>) {
return sycl::ext::oneapi::experimental::exp(x);
} else {
return sycl::exp(x);
}
}
template<typename T>
static __dpct_inline__ T op_silu(T x) {
return x / (static_cast<T>(1.0f) + op_exp(-x));
}
template<typename T>
static __dpct_inline__ T op_erf(T x) {
if constexpr (std::is_same_v<T, sycl::ext::oneapi::bfloat16>) {
+33
View File
@@ -28,6 +28,39 @@ typed_data<T_Dst, T_Src> cast_data(ggml_tensor * dst) {
const float GELU_QUICK_COEF = -1.702f;
// Single-element activations, shared with the mat-vec kernels that fuse a GLU epilogue
// (mmvq.cpp), so both apply the same formula.
template <typename T> static __dpct_inline__ T op_tanh(T x) {
if constexpr (std::is_same_v<T, sycl::ext::oneapi::bfloat16>) {
#if defined(__INTEL_LLVM_COMPILER) && (__INTEL_LLVM_COMPILER >= 20260000)
return sycl::ext::oneapi::experimental::tanh(x);
#else
return static_cast<T>(sycl::tanh(static_cast<float>(x)));
#endif
} else {
return sycl::tanh(x);
}
}
template <typename T> static __dpct_inline__ T op_gelu(T x) {
const T GELU_COEF_A = static_cast<T>(0.044715f);
const T SQRT_2_OVER_PI = static_cast<T>(0.79788456080286535587989211986876f);
return static_cast<T>(0.5f) * x *
(static_cast<T>(1.0f) +
op_tanh(SQRT_2_OVER_PI * x * (static_cast<T>(1.0f) + GELU_COEF_A * x * x)));
}
template <typename T> static __dpct_inline__ T op_exp(T x) {
if constexpr (std::is_same_v<T, sycl::ext::oneapi::bfloat16>) {
return sycl::ext::oneapi::experimental::exp(x);
} else {
return sycl::exp(x);
}
}
template <typename T> static __dpct_inline__ T op_silu(T x) {
return x / (static_cast<T>(1.0f) + op_exp(-x));
}
void ggml_sycl_sqrt(ggml_backend_sycl_context & ctx, ggml_tensor * dst);
+77
View File
@@ -2,6 +2,61 @@
#include <algorithm>
// mul_mat(gate) + mul_mat(up) + GLU: graph shape and tensor properties only. Backend state
// (weight layout, split buffers, DMMV) is checked by ggml_sycl_mul_mat_glu_mmvq_fused().
static bool ggml_sycl_should_fuse_mul_mat_glu(const ggml_tensor * gate, const ggml_tensor * up,
const ggml_tensor * glu) {
// the fused epilogue implements these two; the rest fall back to the standalone GLU kernels
const ggml_glu_op glu_op = ggml_get_glu_op(glu);
if (glu_op != GGML_GLU_OP_SWIGLU && glu_op != GGML_GLU_OP_GEGLU) {
return false;
}
// the kernel always treats src[0] as the activated operand and src[1] as the multiplier
if (ggml_get_op_params_i32(glu, 1) /* swapped */) {
return false;
}
const ggml_tensor * wu = up->src[0];
const ggml_tensor * wg = gate->src[0];
const ggml_tensor * act = up->src[1];
// one set of block offsets and one quantized activation must serve both weights
if (wu->type != wg->type || !ggml_are_same_shape(wu, wg) || !ggml_are_same_stride(wu, wg)) {
return false;
}
if (act != gate->src[1]) {
return false;
}
// only q4_K has a fused reorder GEMV so far, and it walks whole super-blocks
if (wu->type != GGML_TYPE_Q4_K || wu->ne[0] % QK_K != 0) {
return false;
}
// one 2D reorder-layout matrix in, a plain column stride out: no broadcast or padding
if (!ggml_is_contiguous(wu) || !ggml_is_contiguous(wg) || !ggml_is_contiguous(act) ||
!ggml_is_contiguous(glu)) {
return false;
}
if (act->type != GGML_TYPE_F32 || glu->type != GGML_TYPE_F32) {
return false;
}
if (act->ne[2] != 1 || act->ne[3] != 1 || wu->ne[2] != 1 || wu->ne[3] != 1) {
return false;
}
// the kernel writes rows [0, wu->ne[1]) of each glu column, strided by glu->ne[0]
if (glu->ne[0] != wu->ne[1] || glu->ne[1] != act->ne[1]) {
return false;
}
// mat-vec only: one column per decoded token, up to the batch the reorder kernels cover
if (act->ne[1] > MMVQ_MAX_BATCH_SIZE) {
return false;
}
return true;
}
bool ggml_sycl_can_fuse(const ggml_cgraph * cgraph, int node_idx, std::initializer_list<enum ggml_op> ops,
std::initializer_list<enum ggml_unary_op> unary_ops) {
#ifndef NDEBUG
@@ -13,6 +68,28 @@ bool ggml_sycl_can_fuse(const ggml_cgraph * cgraph, int node_idx, std::initializ
return false;
}
// gate and up are siblings, not a chain, so ggml_can_fuse cannot express this: use the
// subgraph form with the GLU as the only materialised output.
if (ops.size() == 3 && ops.begin()[0] == GGML_OP_MUL_MAT && ops.begin()[1] == GGML_OP_MUL_MAT &&
ops.begin()[2] == GGML_OP_GLU) {
if (!ggml_can_fuse_subgraph(cgraph, node_idx, ops, { node_idx + 2 })) {
return false;
}
const ggml_tensor * glu = cgraph->nodes[node_idx + 2];
const ggml_tensor * gate = glu->src[0];
const ggml_tensor * up = glu->src[1];
// don't assume which of the two mat-muls is the gate; infer it from the GLU's operands
const bool ok = (gate == cgraph->nodes[node_idx] && up == cgraph->nodes[node_idx + 1]) ||
(gate == cgraph->nodes[node_idx + 1] && up == cgraph->nodes[node_idx]);
if (!ok) {
return false;
}
return ggml_sycl_should_fuse_mul_mat_glu(gate, up, glu);
}
if (!ggml_can_fuse(cgraph, node_idx, ops)) {
return false;
}
+43 -24
View File
@@ -14,9 +14,9 @@ void gated_delta_net_sycl(const float * q,
const float * beta,
const float * curr_state,
float * dst,
float * state,
int64_t H,
int64_t n_tokens,
int64_t n_seqs,
int64_t sq1,
int64_t sq2,
int64_t sq3,
@@ -29,6 +29,7 @@ void gated_delta_net_sycl(const float * q,
const sycl::uint3 neqk1_magic,
const sycl::uint3 rq3_magic,
float scale,
int64_t state_slot_stride,
int K) {
auto item_ct1 = sycl::ext::oneapi::this_work_item::get_nd_item<3>();
const uint32_t h_idx = item_ct1.get_group(2);
@@ -40,15 +41,12 @@ void gated_delta_net_sycl(const float * q,
const uint32_t iq1 = fastmodulo(h_idx, neqk1_magic);
const uint32_t iq3 = fastdiv(sequence, rq3_magic);
const int64_t attn_score_elems = S_v * H * n_tokens * n_seqs;
float * attn_data = dst;
float * state = dst + attn_score_elems;
// input state holds s0 only [S_v, S_v, H, n_seqs] — seq stride is D = H * S_v * S_v.
// output state layout (per-slot D * n_seqs) — same per-(seq,head) offset as before.
const int64_t state_in_offset = sequence * H * S_v * S_v + h_idx * S_v * S_v;
const int64_t state_out_offset = (sequence * H + h_idx) * S_v * S_v;
const int64_t state_size_per_token = S_v * S_v * H * n_seqs; // per-slot stride in output
state += state_out_offset;
curr_state += state_in_offset + col * S_v;
attn_data += (sequence * n_tokens * H + h_idx) * S_v;
@@ -145,7 +143,7 @@ void gated_delta_net_sycl(const float * q,
if constexpr (keep_rs_t) {
const int target_slot = (int) n_tokens - 1 - t;
if (target_slot >= 0 && target_slot < K) {
float * curr_state = (dst + attn_score_elems) + target_slot * state_size_per_token + state_out_offset;
float * curr_state = state + target_slot * state_slot_stride;
#pragma unroll
for (int r = 0; r < rows_per_lane; r++) {
const int i = r * warp_size + lane;
@@ -172,6 +170,7 @@ static void launch_gated_delta_net(const float * q_d,
const float * b_d,
const float * s_d,
float * dst_d,
float * state_d,
int64_t S_v,
int64_t H,
int64_t n_tokens,
@@ -188,6 +187,7 @@ static void launch_gated_delta_net(const float * q_d,
int64_t neqk1,
int64_t rq3,
float scale,
int64_t state_slot_stride,
int K,
dpct::queue_ptr stream) {
//TODO: Add chunked kernel for even faster pre-fill
@@ -206,9 +206,9 @@ static void launch_gated_delta_net(const float * q_d,
constexpr int sv = 16;
stream->parallel_for(sycl::nd_range<3>(grid_dims * block_dims, block_dims),
[=](sycl::nd_item<3> /*item_ct1*/) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
gated_delta_net_sycl<sv, KDA, keep_rs_t>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, H, n_tokens,
n_seqs, sq1, sq2, sq3, sv1, sv2, sv3, sb1, sb2,
sb3, neqk1_magic, rq3_magic, scale, K);
gated_delta_net_sycl<sv, KDA, keep_rs_t>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, H, n_tokens,
sq1, sq2, sq3, sv1, sv2, sv3, sb1, sb2,
sb3, neqk1_magic, rq3_magic, scale, state_slot_stride, K);
});
}
break;
@@ -217,9 +217,9 @@ static void launch_gated_delta_net(const float * q_d,
constexpr int sv = 32;
stream->parallel_for(sycl::nd_range<3>(grid_dims * block_dims, block_dims),
[=](sycl::nd_item<3> /*item_ct1*/) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
gated_delta_net_sycl<sv, KDA, keep_rs_t>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, H, n_tokens,
n_seqs, sq1, sq2, sq3, sv1, sv2, sv3, sb1, sb2,
sb3, neqk1_magic, rq3_magic, scale, K);
gated_delta_net_sycl<sv, KDA, keep_rs_t>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, H, n_tokens,
sq1, sq2, sq3, sv1, sv2, sv3, sb1, sb2,
sb3, neqk1_magic, rq3_magic, scale, state_slot_stride, K);
});
}
break;
@@ -229,8 +229,8 @@ static void launch_gated_delta_net(const float * q_d,
stream->parallel_for(sycl::nd_range<3>(grid_dims * block_dims, block_dims),
[=](sycl::nd_item<3> /*item_ct1*/) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
gated_delta_net_sycl<sv, KDA, keep_rs_t>(
q_d, k_d, v_d, g_d, b_d, s_d, dst_d, H, n_tokens, n_seqs, sq1, sq2,
sq3, sv1, sv2, sv3, sb1, sb2, sb3, neqk1_magic, rq3_magic, scale, K);
q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, H, n_tokens, sq1, sq2,
sq3, sv1, sv2, sv3, sb1, sb2, sb3, neqk1_magic, rq3_magic, scale, state_slot_stride, K);
});
}
break;
@@ -241,8 +241,8 @@ static void launch_gated_delta_net(const float * q_d,
stream->parallel_for(sycl::nd_range<3>(grid_dims * block_dims, block_dims),
[=](sycl::nd_item<3> /*item_ct1*/) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
gated_delta_net_sycl<sv, KDA, keep_rs_t>(
q_d, k_d, v_d, g_d, b_d, s_d, dst_d, H, n_tokens, n_seqs, sq1, sq2,
sq3, sv1, sv2, sv3, sb1, sb2, sb3, neqk1_magic, rq3_magic, scale, K);
q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d, H, n_tokens, sq1, sq2,
sq3, sv1, sv2, sv3, sb1, sb2, sb3, neqk1_magic, rq3_magic, scale, state_slot_stride, K);
});
}
break;
@@ -253,7 +253,8 @@ static void launch_gated_delta_net(const float * q_d,
}
}
void ggml_sycl_op_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
static void ggml_sycl_op_gated_delta_net_impl(ggml_backend_sycl_context & ctx, ggml_tensor * dst,
const ggml_sycl_gated_delta_net_fused_cache * cache) {
ggml_tensor * src_q = dst->src[0];
ggml_tensor * src_k = dst->src[1];
ggml_tensor * src_v = dst->src[2];
@@ -318,30 +319,48 @@ void ggml_sycl_op_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor *
const int K = ggml_get_op_params_i32(dst, 0);
const bool keep_rs = K > 1;
// recurrent state -> dst tail (after attention scores), or the cache when fusing
float * state_d = dst_d + S_v * H * n_tokens * n_seqs;
int64_t state_slot_stride = S_v * S_v * H * n_seqs;
if (cache != nullptr) {
state_d = cache->data;
state_slot_stride = cache->slot_stride;
}
if (kda) {
if (keep_rs) {
launch_gated_delta_net<true, true>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d,
launch_gated_delta_net<true, true>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d,
S_v, H, n_tokens, n_seqs, sq1, sq2, sq3, sv1, sv2, sv3,
sb1, sb2, sb3, neqk1, rq3, scale, K, stream);
sb1, sb2, sb3, neqk1, rq3, scale, state_slot_stride, K, stream);
} else {
launch_gated_delta_net<true, false>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d,
launch_gated_delta_net<true, false>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d,
S_v, H, n_tokens, n_seqs, sq1, sq2, sq3, sv1, sv2, sv3,
sb1, sb2, sb3, neqk1, rq3, scale, K, stream);
sb1, sb2, sb3, neqk1, rq3, scale, state_slot_stride, K, stream);
}
} else {
if (keep_rs) {
launch_gated_delta_net<false, true>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d,
launch_gated_delta_net<false, true>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d,
S_v, H, n_tokens, n_seqs, sq1, sq2, sq3, sv1, sv2, sv3,
sb1, sb2, sb3, neqk1, rq3, scale, K, stream);
sb1, sb2, sb3, neqk1, rq3, scale, state_slot_stride, K, stream);
} else {
launch_gated_delta_net<false, false>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d,
launch_gated_delta_net<false, false>(q_d, k_d, v_d, g_d, b_d, s_d, dst_d, state_d,
S_v, H, n_tokens, n_seqs, sq1, sq2, sq3, sv1, sv2, sv3,
sb1, sb2, sb3, neqk1, rq3, scale, K, stream);
sb1, sb2, sb3, neqk1, rq3, scale, state_slot_stride, K, stream);
}
}
}
void ggml_sycl_op_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
ggml_sycl_op_gated_delta_net_impl(ctx, dst, nullptr);
}
void ggml_sycl_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/6);
ggml_sycl_op_gated_delta_net(ctx, dst);
}
void ggml_sycl_op_gated_delta_net_fused_cache(ggml_backend_sycl_context & ctx, ggml_tensor * dst,
ggml_sycl_gated_delta_net_fused_cache cache) {
scope_op_debug_print scope_dbg_print(__func__, dst, /*num_src=*/6);
ggml_sycl_op_gated_delta_net_impl(ctx, dst, &cache);
}
+10
View File
@@ -5,5 +5,15 @@
#include "common.hpp"
#include "ggml.h"
// fused-kernel recurrent-state output; strides in elements (per-seq stride is always D, set in-kernel)
struct ggml_sycl_gated_delta_net_fused_cache {
float * data; // rollback slot 0
int64_t slot_stride; // between rollback slots (0 when K==1)
};
void ggml_sycl_op_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor * dst);
void ggml_sycl_gated_delta_net(ggml_backend_sycl_context & ctx, ggml_tensor * dst);
// same op, but writes the snapshot(s) into the cache instead of dst (see ggml_sycl_try_gdn_cache_fusion)
void ggml_sycl_op_gated_delta_net_fused_cache(ggml_backend_sycl_context & ctx, ggml_tensor * dst,
ggml_sycl_gated_delta_net_fused_cache cache);
+202 -11
View File
@@ -11,6 +11,7 @@
//
#include <algorithm>
#include <array>
#include <assert.h>
#include <atomic>
#include <cinttypes>
@@ -101,6 +102,7 @@ int g_ggml_sycl_use_level_zero_api = 0;
int g_ggml_sycl_enable_flash_attention = 1;
int g_ggml_sycl_dev2dev_memcpy = DEV2DEV_MEMCPY_SYCL;
int g_ggml_sycl_usm_system = 0;
int g_ggml_sycl_enable_host_pinned_mem = 1;
static ggml_sycl_device_info ggml_sycl_init() {
ggml_sycl_device_info info = {};
@@ -317,6 +319,8 @@ static void ggml_check_sycl() try {
#endif
g_ggml_sycl_usm_system = ggml_sycl_get_env("GGML_SYCL_USM_SYSTEM", 0);
g_ggml_sycl_enable_host_pinned_mem =
ggml_sycl_get_env("GGML_SYCL_ENABLE_HOST_PINNED_MEM", 1);
GGML_SYCL_DEBUG("[SYCL] call ggml_check_sycl\n");
@@ -415,6 +419,7 @@ static void ggml_check_sycl() try {
#endif
GGML_LOG_INFO(" GGML_SYCL_USM_SYSTEM: %d\n", g_ggml_sycl_usm_system);
GGML_LOG_INFO(" GGML_SYCL_ENABLE_HOST_PINNED_MEM: %d\n", g_ggml_sycl_enable_host_pinned_mem);
/* NOT REMOVE, keep it for next optimize for XMX.
#if defined(SYCL_USE_XMX)
@@ -1442,18 +1447,53 @@ ggml_backend_buffer_type_t ggml_backend_sycl_split_buffer_type(const float * ten
// host buffer type
struct ggml_backend_sycl_device_context {
int device;
std::string name;
std::string description;
int op_offload_min_batch_size;
};
static const char * ggml_backend_sycl_host_buffer_type_name(ggml_backend_buffer_type_t buft) {
return GGML_SYCL_NAME "_Host";
GGML_UNUSED(buft);
}
//host pinned memory
static void * ggml_backend_sycl_host_malloc(size_t size) {
void * ptr = nullptr;
try {
ggml_check_sycl();
// USM host memory is page-locked and device-accessible by construction
auto & q = dpct::dev_mgr::instance().get_device(0).default_queue();
ptr = sycl::malloc_host(size, q, sycl::property_list{});
} catch (...) {
ptr = nullptr;
}
if (ptr == nullptr) {
GGML_LOG_WARN("%s: failed to allocate %.2f MiB of pinned memory\n", __func__,
size / 1024.0 / 1024.0);
}
return ptr;
}
static void ggml_backend_sycl_host_buffer_free_buffer(ggml_backend_buffer_t buffer) {
free_aligned_mem_host((void *)buffer->context);
if (buffer->context == nullptr) {
return;
}
if (g_ggml_sycl_enable_host_pinned_mem) {
auto & q = dpct::dev_mgr::instance().get_device(0).default_queue();
SYCL_CHECK(CHECK_TRY_ERROR(sycl::free(buffer->context, q)));
} else {
free_aligned_mem_host((void *) buffer->context);
}
}
static ggml_backend_buffer_t ggml_backend_sycl_host_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) {
void * ptr = aligned_malloc_host(TENSOR_ALIGNMENT, size);
void * ptr = g_ggml_sycl_enable_host_pinned_mem ? ggml_backend_sycl_host_malloc(size) :
aligned_malloc_host(TENSOR_ALIGNMENT, size);
if (ptr == nullptr) {
// fallback to cpu buffer
return ggml_backend_buft_alloc_buffer(ggml_backend_cpu_buffer_type(), size);
@@ -1467,6 +1507,11 @@ static ggml_backend_buffer_t ggml_backend_sycl_host_buffer_type_alloc_buffer(ggm
return buffer;
}
static size_t ggml_backend_sycl_host_buffer_type_get_max_size(ggml_backend_buffer_type_t buft) {
ggml_backend_sycl_device_context * dev_ctx = (ggml_backend_sycl_device_context *) buft->device->context;
return dpct::dev_mgr::instance().get_device(dev_ctx->device).get_max_mem_alloc_size();
}
ggml_backend_buffer_type_t ggml_backend_sycl_host_buffer_type() {
GGML_SYCL_DEBUG("[SYCL] call ggml_backend_sycl_host_buffer_type\n");
static struct ggml_backend_buffer_type ggml_backend_sycl_buffer_type_host = {
@@ -1474,7 +1519,7 @@ ggml_backend_buffer_type_t ggml_backend_sycl_host_buffer_type() {
/* .get_name = */ ggml_backend_sycl_host_buffer_type_name,
/* .alloc_buffer = */ ggml_backend_sycl_host_buffer_type_alloc_buffer,
/* .get_alignment = */ ggml_backend_cpu_buffer_type()->iface.get_alignment,
/* .get_max_size = */ NULL, // TODO: return device.maxBufferLength
/* .get_max_size = */ ggml_backend_sycl_host_buffer_type_get_max_size,
/* .get_alloc_size = */ ggml_backend_cpu_buffer_type()->iface.get_alloc_size,
/* .is_host = */ ggml_backend_cpu_buffer_type()->iface.is_host,
},
@@ -4516,6 +4561,66 @@ static void ggml_sycl_mul_mat(ggml_backend_sycl_context & ctx, const ggml_tensor
}
}
// Fused dense-FFN mat-vec for the {mul_mat(gate), mul_mat(up), GLU} subgraph at node_idx.
// Returns false if it declined, in which case the caller runs the three nodes normally.
static bool ggml_sycl_mul_mat_glu_mmvq_fused(ggml_backend_sycl_context & ctx, ggml_cgraph * cgraph, int node_idx) {
if (!ggml_sycl_can_fuse(cgraph, node_idx, { GGML_OP_MUL_MAT, GGML_OP_MUL_MAT, GGML_OP_GLU }, {})) {
return false;
}
ggml_tensor * glu = cgraph->nodes[node_idx + 2];
ggml_tensor * gate = glu->src[0];
ggml_tensor * up = glu->src[1];
const ggml_tensor * wu = up->src[0];
const ggml_tensor * wg = gate->src[0];
const ggml_tensor * act = up->src[1];
// this writes glu->data directly rather than the per-device row slices that
// ggml_sycl_op_mul_mat() stitches back together, so it cannot serve split weights
if (ggml_backend_buffer_is_sycl_split(wu->buffer) || ggml_backend_buffer_is_sycl_split(wg->buffer)) {
return false;
}
// with DMMV prioritised the unfused path would not have gone through mmvq at all
if (g_ggml_sycl_prioritize_dmmv) {
return false;
}
// install the reorder (SoA) layout the fused kernel needs, as the unfused mmvq path would;
// a no-op once done. after the bail checks so a declined op does not pay for it.
opt_for_reorder(&ctx, wu, act, up, mul_mat_algo::MMVQ);
opt_for_reorder(&ctx, wg, act, gate, mul_mat_algo::MMVQ);
const auto * extra_u = static_cast<const ggml_tensor_extra_gpu *>(wu->extra);
const auto * extra_g = static_cast<const ggml_tensor_extra_gpu *>(wg->extra);
if (!extra_u || !extra_g || !extra_u->optimized_feature.reorder || !extra_g->optimized_feature.reorder) {
return false;
}
// log the up mat-mul: glu's own srcs are the two intermediates the fusion never materialises
scope_op_debug_print scope_dbg_print(__func__, up, /*num_src=*/2, " : fused with gate + GLU");
const int64_t ne00 = wu->ne[0];
const int64_t ne11 = act->ne[1];
const queue_ptr stream = ctx.stream();
const int src1_padded_cols = GGML_PAD((int) ne00, MATRIX_ROW_PADDING);
// one activation, quantized once and fully consumed into src1_ddq before the GEMV on this
// in-order queue, so glu->data aliasing the dead activation needs no memory-range check
ggml_sycl_pool_alloc<char> src1_q8_alloc(ctx.pool(),
(size_t) ne11 * src1_padded_cols * sizeof(block_q8_1) / QK8_1);
char * src1_ddq = src1_q8_alloc.get();
quantize_row_q8_1_sycl<quantize_and_reorder_q8_1_soa>((const float *) act->data, src1_ddq, (int) ne00, (int) ne11,
src1_padded_cols, stream);
return ggml_sycl_mul_mat_vec_q_glu_reorder(wu->type, ggml_get_glu_op(glu), wu->data, wg->data, src1_ddq,
(float *) glu->data, (int) ne00, (int) wu->ne[1], (int) ne11,
/*stride_col_y_bytes=*/src1_padded_cols * (int) sizeof(block_q8_1) /
QK8_1,
/*stride_col_dst=*/(int) glu->ne[0], stream);
}
__dpct_inline__ static void k_copy_src1_to_contiguous(
const char *__restrict__ src1_original, char *__restrict__ src1_contiguous,
@@ -5420,12 +5525,90 @@ catch (sycl::exception const &exc) {
std::exit(1);
}
static bool ggml_sycl_is_view_or_noop(const ggml_tensor * t) {
return ggml_is_empty(t) || t->op == GGML_OP_RESHAPE || t->op == GGML_OP_TRANSPOSE ||
t->op == GGML_OP_VIEW || t->op == GGML_OP_PERMUTE || t->op == GGML_OP_NONE;
}
// match gated_delta_net + the strided cpy that scatters its state snapshots into the cache
// (slot i -> rollback group i, slot 0 newest), so the kernel can write them and skip the cpy.
// returns the number of following nodes to skip (0 = no fusion)
// ported from ggml_cuda_try_gdn_cache_fusion - pure graph inspection, backend-agnostic
static int ggml_sycl_try_gdn_cache_fusion(const ggml_cgraph * cgraph, int node_idx,
ggml_sycl_gated_delta_net_fused_cache & fused_state_cpy) {
if (!g_ggml_sycl_enable_fusion) {
return 0;
}
const ggml_tensor * gdn = cgraph->nodes[node_idx];
// the kernel skips the snapshot tail, so the gdn output must not be a graph output, and the cpy
// found below is taken to be its only reader, as it is in every graph that builds this op
if (gdn->op != GGML_OP_GATED_DELTA_NET || gdn->type != GGML_TYPE_F32 ||
(gdn->flags & GGML_TENSOR_FLAG_OUTPUT)) {
return 0;
}
const ggml_tensor * src_v = gdn->src[2];
const int64_t S_v = src_v->ne[0];
const int64_t H = src_v->ne[1];
const int64_t n_tokens = src_v->ne[2];
const int64_t n_seqs = src_v->ne[3];
const int64_t D = S_v * S_v * H;
const int64_t K = ggml_get_op_params_i32(gdn, 0); // snapshot slot count
const int64_t n_written = std::min<int64_t>(n_tokens, K); // newest n_written slots are written
// snapshot tail starts right after the attention scores
const size_t tail_off = ggml_row_size(GGML_TYPE_F32, S_v * H * n_tokens * n_seqs);
// the cpy must be the first node the compute loop below runs, so nothing can read the cache first.
// skip exactly what that loop skips: views, no-ops, and nodes the graph does not compute.
const ggml_tensor * cpy = nullptr;
int skip = 0;
for (int j = node_idx + 1; j < cgraph->n_nodes && cpy == nullptr; ++j) {
const ggml_tensor * n = cgraph->nodes[j];
if (ggml_sycl_is_view_or_noop(n) || (n->flags & GGML_TENSOR_FLAG_COMPUTE) == 0) {
continue;
}
if (n->op != GGML_OP_CPY || (n->flags & GGML_TENSOR_FLAG_OUTPUT)) {
return 0;
}
cpy = n;
skip = j - node_idx;
}
if (cpy == nullptr) {
return 0;
}
const ggml_tensor * src = cpy->src[0]; // view of the gdn snapshot tail
const ggml_tensor * dst = cpy->src[1]; // cache view the kernel writes to
// src must be this gdn's snapshot tail (contiguous, at the tail offset)
if (src->op != GGML_OP_VIEW || src->view_src != gdn || src->view_offs != tail_off ||
!ggml_is_contiguous(src)) {
return 0;
}
// dst is the [D, n_seqs, n_written] cache view, with the per-seq stride D that the kernel assumes.
// ggml_cpy pins src to the same element count, so src needs no shape check of its own.
const std::array<int64_t, GGML_MAX_DIMS> expected_ne = { D, n_seqs, n_written, 1 };
if (dst->op != GGML_OP_VIEW || dst->type != GGML_TYPE_F32 || dst->data == nullptr ||
!std::equal(expected_ne.begin(), expected_ne.end(), dst->ne) ||
dst->nb[0] != ggml_type_size(GGML_TYPE_F32) ||
dst->nb[1] != (size_t) ggml_row_size(GGML_TYPE_F32, D)) {
return 0;
}
fused_state_cpy.data = (float *) dst->data; // rollback group 0 (newest)
fused_state_cpy.slot_stride = K > 1 ? (int64_t) (dst->nb[2] / sizeof(float)) : 0;
return skip;
}
static void ggml_backend_sycl_graph_compute_impl(ggml_backend_sycl_context * sycl_ctx, ggml_cgraph * cgraph) {
ggml_sycl_set_main_device(sycl_ctx->device);
for (int i = 0; i < cgraph->n_nodes; i++) {
ggml_tensor * node = cgraph->nodes[i];
if (ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE) {
if (ggml_sycl_is_view_or_noop(node)) {
continue;
}
if ((node->flags & GGML_TENSOR_FLAG_COMPUTE) == 0) {
@@ -5445,6 +5628,16 @@ static void ggml_backend_sycl_graph_compute_impl(ggml_backend_sycl_context * syc
}
}
#endif
// gated_delta_net -> cpy: scatter recurrent-state snapshots into the cache
if (node->op == GGML_OP_GATED_DELTA_NET) {
ggml_sycl_gated_delta_net_fused_cache fused_state_cpy;
const int gdn_nodes_to_skip = ggml_sycl_try_gdn_cache_fusion(cgraph, i, fused_state_cpy);
if (gdn_nodes_to_skip > 0) {
ggml_sycl_op_gated_delta_net_fused_cache(*sycl_ctx, node, fused_state_cpy);
i += gdn_nodes_to_skip;
continue;
}
}
if (node->op == GGML_OP_RMS_NORM &&
ggml_sycl_can_fuse(cgraph, i, { GGML_OP_RMS_NORM, GGML_OP_MUL }, {})) {
ggml_sycl_op_rms_norm_fused(*sycl_ctx, node, cgraph->nodes[i + 1]);
@@ -5458,6 +5651,11 @@ static void ggml_backend_sycl_graph_compute_impl(ggml_backend_sycl_context * syc
continue;
}
if (node->op == GGML_OP_MUL_MAT && ggml_sycl_mul_mat_glu_mmvq_fused(*sycl_ctx, cgraph, i)) {
i += 2;
continue;
}
bool ok = ggml_sycl_compute_forward(*sycl_ctx, node);
if (!ok) {
GGML_LOG_ERROR("%s: error: op not supported %s (%s)\n", __func__, node->name, ggml_op_name(node->op));
@@ -5628,13 +5826,6 @@ int ggml_backend_sycl_get_device_count() {
// backend device
struct ggml_backend_sycl_device_context {
int device;
std::string name;
std::string description;
int op_offload_min_batch_size;
};
static const char * ggml_backend_sycl_device_get_name(ggml_backend_dev_t dev) {
ggml_backend_sycl_device_context * ctx = (ggml_backend_sycl_device_context *)dev->context;
return ctx->name.c_str();
+117 -15
View File
@@ -2,6 +2,7 @@
#include "ggml.h"
#include "common.hpp"
#include "element_wise.hpp"
#include "quants.hpp"
#include "vecdotq.hpp"
@@ -56,11 +57,13 @@ static void mul_mat_vec_q_reorder(const void * __restrict__ vx, const void * __r
}
}
template <typename reorder_vec_dot_q_sycl, int ncols_dst>
static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void * __restrict__ vy,
float * __restrict__ dst, const int ncols, const int nrows,
const int stride_col_y_bytes, const int stride_col_dst,
const sycl::nd_item<3> & nd_item) {
// With has_fusion, `vgate` is a second weight matrix sharing vx's shape, stride and reorder
// layout: one pass computes both row dot products and the epilogue writes glu(gate, up).
template <typename reorder_vec_dot_q_sycl, int ncols_dst, bool has_fusion = false>
static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void * __restrict__ vgate,
const void * __restrict__ vy, float * __restrict__ dst, const int ncols,
const int nrows, const int stride_col_y_bytes, const int stride_col_dst,
const ggml_glu_op glu_op, const sycl::nd_item<3> & nd_item) {
using block_type = ggml_sycl_reordered::block_q_t<reorder_vec_dot_q_sycl::gtype>;
using block_traits = typename block_type::traits;
@@ -70,6 +73,8 @@ static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void
const int sg_id = sg.get_group_linear_id();
const int row = workgroup_id * sg_range + sg_id;
// row is sub-group uniform, so this retires whole sub-groups and the collectives below
// stay convergent
if (row >= nrows) {
return;
}
@@ -82,10 +87,15 @@ static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void
static_assert(blocks_per_subgroup > 0);
static_assert(block_elements_per_subgroup > 0);
float partial_sum[ncols_dst] = {0.0f};
float partial_sum[ncols_dst] = { 0.0f };
// sized 1 rather than 0 when unused: zero-length arrays are not standard C++, and the
// array is dead and eliminated in that case
[[maybe_unused]] float partial_gate[has_fusion ? ncols_dst : 1] = { 0.0f };
for (int i = sg.get_local_linear_id() / block_elements_per_subgroup; i < blocks_per_row; i += blocks_per_subgroup) {
const int ibx = row * blocks_per_row + i;
// the offsets depend only on the block index and the matrix shape, never on the base
// pointer, which is what lets vgate reuse them
const auto bx_offset = block_type::get_block_offset(ibx, nblocks);
const auto d_offset = block_type::get_d_offset(nrows, ncols, ibx);
const int iby = i * block_type::block_to_q8_1_ratio();
@@ -96,11 +106,16 @@ static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void
#pragma unroll
for (int j = 0; j < ncols_dst; ++j) {
const char * vy_j = (const char *)vy + j * stride_col_y_bytes;
const int8_t * q8_1_quant_ptr = (const int8_t *)vy_j + iby * QK8_1;
const sycl::half2* q8_1_ds_ptr = (const sycl::half2 *)(vy_j + ncols + iby * sizeof(sycl::half2));
const char * vy_j = (const char *) vy + j * stride_col_y_bytes;
const int8_t * q8_1_quant_ptr = (const int8_t *) vy_j + iby * QK8_1;
const sycl::half2 * q8_1_ds_ptr = (const sycl::half2 *) (vy_j + ncols + iby * sizeof(sycl::half2));
partial_sum[j] += reorder_vec_dot_q_sycl()(vx, bx_offset, d_offset, q8_1_quant_ptr, q8_1_ds_ptr, iqs);
if constexpr (has_fusion) {
partial_gate[j] +=
reorder_vec_dot_q_sycl()(vgate, bx_offset, d_offset, q8_1_quant_ptr, q8_1_ds_ptr, iqs);
}
}
}
}
@@ -109,6 +124,13 @@ static void mul_mat_vec_q_reorder_ncols(const void * __restrict__ vx, const void
for (int j = 0; j < ncols_dst; ++j) {
float sum = sycl::reduce_over_group(nd_item.get_sub_group(), partial_sum[j], std::plus<>());
if constexpr (has_fusion) {
const float gate = sycl::reduce_over_group(nd_item.get_sub_group(), partial_gate[j], std::plus<>());
// uniform across the launch; the launcher only instantiates SWIGLU and GEGLU
sum *= glu_op == GGML_GLU_OP_SWIGLU ? op_silu(gate) : op_gelu(gate);
}
if (sg.leader()) {
dst[j * stride_col_dst + row] = sum;
}
@@ -691,7 +713,8 @@ static void reorder_mul_mat_vec_q4_0_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q4_0>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -1108,7 +1131,8 @@ static void reorder_mul_mat_vec_q8_0_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q8_0>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -1436,7 +1460,8 @@ static void reorder_mul_mat_vec_q3_k_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q3_K>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -1604,7 +1629,8 @@ static void reorder_mul_mat_vec_q4_k_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q4_K>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -1731,7 +1757,8 @@ static void reorder_mul_mat_vec_q5_k_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q5_K>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -1789,7 +1816,8 @@ static void reorder_mul_mat_vec_q6_k_q8_1_sycl_ncols(
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl<GGML_TYPE_Q6_K>, ncols_dst>(
vx, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, nd_item);
vx, /*vgate=*/ nullptr, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst,
/*glu_op=*/ GGML_GLU_OP_SWIGLU, nd_item);
});
});
}
@@ -2736,3 +2764,77 @@ bool ggml_sycl_mul_mat_vec_q_id_reorder(
return false;
}
}
template <typename reorder_vec_dot_q_sycl, int ncols_dst>
static void launch_mul_mat_vec_q_reorder_glu(const void * vx, const void * vgate, const void * vy, float * dst,
const int ncols, const int nrows, const int stride_col_y_bytes,
const int stride_col_dst, const ggml_glu_op glu_op,
dpct::queue_ptr stream) {
GGML_ASSERT(ncols % QK_K == 0);
constexpr size_t num_subgroups = WARP_SIZE;
const int block_num_y = ceil_div(nrows, GGML_SYCL_MMV_Y * (int) num_subgroups);
const sycl::range<3> block_nums(1, 1, block_num_y);
const sycl::range<3> block_dims(1, GGML_SYCL_MMV_Y, num_subgroups * WARP_SIZE);
stream->submit([&](sycl::handler & cgh) {
cgh.parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
[=](sycl::nd_item<3> nd_item) [[sycl::reqd_sub_group_size(WARP_SIZE)]] {
mul_mat_vec_q_reorder_ncols<reorder_vec_dot_q_sycl, ncols_dst, /*has_fusion=*/ true>(
vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes, stride_col_dst, glu_op,
nd_item);
});
});
}
bool ggml_sycl_mul_mat_vec_q_glu_reorder(enum ggml_type src0_type, enum ggml_glu_op glu_op, const void * vx,
const void * vgate, const void * vy, float * dst, int ncols, int nrows,
int ncols_dst, int stride_col_y_bytes, int stride_col_dst,
dpct::queue_ptr stream) {
if (src0_type != GGML_TYPE_Q4_K) {
return false;
}
if (glu_op != GGML_GLU_OP_SWIGLU && glu_op != GGML_GLU_OP_GEGLU) {
return false;
}
using vec_dot = reorder_vec_dot_q_sycl<GGML_TYPE_Q4_K>;
switch (ncols_dst) {
case 1:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 1>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 2:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 2>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 3:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 3>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 4:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 4>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 5:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 5>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 6:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 6>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 7:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 7>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
case 8:
launch_mul_mat_vec_q_reorder_glu<vec_dot, 8>(vx, vgate, vy, dst, ncols, nrows, stride_col_y_bytes,
stride_col_dst, glu_op, stream);
return true;
default:
return false;
}
}
+16
View File
@@ -57,4 +57,20 @@ bool ggml_sycl_mul_mat_vec_q_id_reorder(
size_t src1_row_stride,
dpct::queue_ptr stream);
// Fused dense-FFN GEMV: writes glu(gate . y, up . y) instead of the two mat-vec results.
// vx / vgate must share shape, stride and reorder layout. Returns false if unhandled.
bool ggml_sycl_mul_mat_vec_q_glu_reorder(
enum ggml_type src0_type,
enum ggml_glu_op glu_op,
const void * vx,
const void * vgate,
const void * vy,
float * dst,
int ncols, // K, shared by both weights
int nrows, // output rows, i.e. weight ne[1]
int ncols_dst, // activation columns, 1..MMVQ_MAX_BATCH_SIZE
int stride_col_y_bytes, // bytes between activation columns in vy
int stride_col_dst, // floats between output columns in dst
dpct::queue_ptr stream);
#endif // GGML_SYCL_MMVQ_HPP
-1
View File
@@ -1022,7 +1022,6 @@ bool llm_arch_supports_sm_tensor(const llm_arch & arch) {
case LLM_ARCH_OLMOE:
case LLM_ARCH_DEEPSEEK2:
case LLM_ARCH_DEEPSEEK32:
case LLM_ARCH_DEEPSEEK4:
case LLM_ARCH_GLM_DSA:
case LLM_ARCH_BITNET:
case LLM_ARCH_T5:
+65 -3
View File
@@ -363,9 +363,13 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
static const std::regex pattern_qkv_bias ("blk\\.\\d*\\.attn_qkv.bias");
static const std::regex pattern_qk_norm ("blk\\.\\d*\\.attn_(q|k)_norm\\.weight");
static const std::regex pattern_kv_cache ("cache_(k|v)_l\\d*");
static const std::regex pattern_dsv4_state ("dsv4_(csa|hca|lid)_state_(kv|score)_l\\d*");
static const std::regex pattern_attn_sinks ("blk\\.\\d*\\.attn_sinks.weight");
static const std::regex pattern_attn_out_weight ("blk\\.\\d*\\.attn_output.weight");
static const std::regex pattern_attn_out_bias ("blk\\.\\d*\\.attn_output.bias");
static const std::regex pattern_attn_out_a_weight("blk\\.\\d*\\.attn_output_a\\.weight");
static const std::regex pattern_attn_out_b_weight("blk\\.\\d*\\.attn_output_b\\.weight");
static const std::regex pattern_attn_q_b_weight ("blk\\.\\d*\\.attn_q_b\\.weight");
static const std::regex pattern_attn_gate_weight("blk\\.\\d*\\.attn_gate.weight");
static const std::regex pattern_ssm_dt ("blk\\.\\d*\\.ssm_dt.bias");
@@ -384,8 +388,11 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
static const std::regex pattern_ffn_gate_bias ("blk\\.\\d*\\.ffn_gate(_exps)?.bias");
static const std::regex pattern_ffn_gate_up_weight("blk\\.\\d*\\.ffn_gate_up(_exps)?.weight");
static const std::regex pattern_ffn_down_weight ("blk\\.\\d*\\.ffn_down(_exps)?.weight");
static const std::regex pattern_ffn_down_bias ("blk\\.\\d*\\.ffn_down.bias");
static const std::regex pattern_ffn_down_exps_bias("blk\\.\\d*\\.ffn_down_exps.bias");
static const std::regex pattern_ffn_down_bias ("blk\\.\\d*\\.ffn_down.bias");
static const std::regex pattern_ffn_down_exps_bias ("blk\\.\\d*\\.ffn_down_exps.bias");
static const std::regex pattern_ffn_up_shexp_weight ("blk\\.\\d*\\.ffn_up_shexp.weight");
static const std::regex pattern_ffn_gate_shexp_weight ("blk\\.\\d*\\.ffn_gate_shexp.weight");
static const std::regex pattern_ffn_down_shexp_weight ("blk\\.\\d*\\.ffn_down_shexp.weight");
static const std::regex pattern_output_weight("output\\.weight");
static const std::regex pattern_output_bias ("output\\.bias");
@@ -442,6 +449,37 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
};
auto get_tensor_config = [&]() -> tensor_config {
// dflash drafters are small, mirror them on every device: no reduction boundaries,
// and the target hidden-state handoff stays within the same backends
if (ud->model->arch == LLM_ARCH_DFLASH) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED);
}
if (ud->model->arch == LLM_ARCH_DEEPSEEK4) {
if (std::regex_match(tensor_name, pattern_kv_cache) ||
std::regex_match(tensor_name, pattern_dsv4_state)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED);
}
if (std::regex_match(tensor_name, pattern_attn_sinks)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0, "attn_output_a.weight");
}
if (std::regex_match(tensor_name, pattern_attn_q_b_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "attn_output_a.weight");
}
if (std::regex_match(tensor_name, pattern_attn_out_a_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_2, "attn_output_b.weight");
}
if (std::regex_match(tensor_name, pattern_attn_out_b_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0);
}
if (std::regex_match(tensor_name, pattern_ffn_up_shexp_weight) ||
std::regex_match(tensor_name, pattern_ffn_gate_shexp_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "ffn_down_shexp.weight");
}
if (std::regex_match(tensor_name, pattern_ffn_down_shexp_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_0, "ffn_down_shexp.weight");
}
}
// standard attention
if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_kv_weight)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_1, "attn_output.weight", "ssm_out.weight");
@@ -629,9 +667,29 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
if (std::regex_match(tensor_name, pattern_attn_sinks)) {
GGML_ASSERT(segments.size() == 1);
if (ud->model->arch == LLM_ARCH_DEEPSEEK4) {
return {hparams.n_head(il) / hparams.dsv4_o_group_count};
}
return {std::lcm(n_embd_q, blck_size_perf)/n_embd_q * n_gqa};
}
if (ud->model->arch == LLM_ARCH_DEEPSEEK4) {
if (std::regex_match(tensor_name, pattern_attn_q_b_weight)) {
GGML_ASSERT(segments.size() == 1);
// the grouped output projection requires each device to hold whole groups of heads
const int64_t n_head_group = hparams.n_head(il) / hparams.dsv4_o_group_count;
return {n_head_group * hparams.n_embd_head_k(il)};
}
if (std::regex_match(tensor_name, pattern_attn_out_a_weight)) {
GGML_ASSERT(segments.size() == 1);
return {1};
}
if (std::regex_match(tensor_name, pattern_attn_out_b_weight)) {
GGML_ASSERT(segments.size() == 1);
return {std::lcm<int64_t>(hparams.dsv4_o_lora_rank, blck_size)};
}
}
const int64_t granularity_q = std::lcm(n_embd_q, blck_size_perf);
if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_q_bias)) {
GGML_ASSERT(segments.size() == 1);
@@ -662,7 +720,11 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
// FFN
if (std::regex_match(tensor_name, pattern_ffn_up_weight) || std::regex_match(tensor_name, pattern_ffn_up_bias) ||
std::regex_match(tensor_name, pattern_ffn_gate_weight) || std::regex_match(tensor_name, pattern_ffn_gate_bias) ||
std::regex_match(tensor_name, pattern_ffn_gate_up_weight) || std::regex_match(tensor_name, pattern_ffn_down_weight)) {
std::regex_match(tensor_name, pattern_ffn_gate_up_weight) ||
std::regex_match(tensor_name, pattern_ffn_down_weight) ||
std::regex_match(tensor_name, pattern_ffn_up_shexp_weight) ||
std::regex_match(tensor_name, pattern_ffn_gate_shexp_weight) ||
std::regex_match(tensor_name, pattern_ffn_down_shexp_weight)) {
const int64_t blck_size_perf = std::lcm(blck_size, 128);
GGML_ASSERT(segments.size() == 1);
return {blck_size_perf};
+5 -1
View File
@@ -257,7 +257,11 @@ static bool llama_prepare_model_devices(const llama_model_params & params, llama
}
case GGML_BACKEND_DEVICE_TYPE_IGPU:
if (igpus.empty()) {
// igpus.empty() - workaround for integrated devices seen by multiple backends
// ref: https://github.com/ggml-org/llama.cpp/pull/23897
// ggml_backend_dev_backend_reg - allow devices of the same backend regardless if integrated
// ref: https://github.com/ggml-org/llama.cpp/pull/23897#issuecomment-5264222997
if (igpus.empty() || ggml_backend_dev_backend_reg(dev) == ggml_backend_dev_backend_reg(igpus.back().dev)) {
igpus.push_back({false, dev});
}
break;
+7 -4
View File
@@ -14,11 +14,14 @@ void llama_model_dflash::load_arch_hparams(llama_model_loader & ml) {
hparams.n_embd_inp_enc_impl = (uint32_t) target_layer_ids.size() * hparams.n_embd;
LLAMA_LOG_INFO("%s: DFlash extract_layers = [", __func__);
for (size_t i = 0; i < target_layer_ids.size(); ++i) {
LLAMA_LOG_INFO("%d%s", target_layer_ids[i], i + 1 < target_layer_ids.size() ? ", " : "");
std::string layers;
const char * sep = "";
for (const auto id : target_layer_ids) {
layers += sep;
layers += std::to_string(id);
sep = ", ";
}
LLAMA_LOG_INFO("]\n");
LLAMA_LOG_INFO("%s: DFlash extract_layers = [%s]\n", __func__, layers.c_str());
// DeepSeek-V4 DSpark backbone: stages are full DSV4 blocks, uniform sliding window (the draft KV ring)
ml.get_key(LLM_KV_HYPER_CONNECTION_COUNT, hparams.dsv4_hc_mult, false);
+7
View File
@@ -9804,6 +9804,13 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
use_id, 16, 8, b, with_bias, with_gate, with_lane_scale));
test_cases.emplace_back(new test_mul_mat_vec_fusion(type, glu_op, 1, 32, 256,
use_id, 16, 8, b, with_bias, with_gate, with_lane_scale, {1, 1}));
if (!use_id && with_gate && !with_bias) {
// small multi-token batches (speculative decoding / MTP verify)
for (int64_t m_batch : { 2, 4, 8 }) {
test_cases.emplace_back(new test_mul_mat_vec_fusion(type, glu_op, m_batch, 32, 256,
use_id, 16, 8, b, with_bias, with_gate, with_lane_scale, {1, 1}));
}
}
}
}
}
+2 -2
View File
@@ -4618,7 +4618,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
// Real life test - execute_command
tst.test("<|tool_call_begin|>functions.execute_command:0<|tool_call_argument_begin|>{\"command\": \"ls -lah\""
", \"cwd\": \"/home/jarvis/development/exllamav3\", \"timeout\": 10}")
", \"cwd\": \"/home/user/development/exllamav3\", \"timeout\": 10}")
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
.parallel_tool_calls(true)
.tools({
@@ -4648,7 +4648,7 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
expect_tool_calls({
{
"execute_command",
R"({"command": "ls -lah", "cwd": "/home/jarvis/development/exllamav3", "timeout": 10})",
R"({"command": "ls -lah", "cwd": "/home/user/development/exllamav3", "timeout": 10})",
"functions.execute_command:0"
}
})
+41 -10
View File
@@ -101,6 +101,12 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
n_head = 1;
n_ff = 96;
n_layer = 22; // hparams.n_layer_kv_from_start = 20 is hardcoded
} else if (arch == LLM_ARCH_DEEPSEEK4) {
// head size 64 so that GPU flash attention kernels support the model
n_embd = 512;
n_head = 8;
n_ff = 1024;
n_layer = 4;
} else if (arch == LLM_ARCH_DEEPSEEK2
|| arch == LLM_ARCH_DEEPSEEK32
|| arch == LLM_ARCH_GLM_DSA
@@ -156,11 +162,15 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head_per_layer);
} else {
ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT, n_head);
ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, n_head);
ms.add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(1) : n_head);
}
ms.add_kv(LLM_KV_ATTENTION_MAX_ALIBI_BIAS, 8.0f);
if (arch == LLM_ARCH_DEEPSEEK2
if (arch == LLM_ARCH_DEEPSEEK4) {
ms.add_kv(LLM_KV_ATTENTION_KEY_LENGTH, n_embd_head);
ms.add_kv(LLM_KV_ATTENTION_VALUE_LENGTH, n_embd_head);
ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, n_embd_head/2);
} else if (arch == LLM_ARCH_DEEPSEEK2
|| arch == LLM_ARCH_DEEPSEEK32
|| arch == LLM_ARCH_GLM_DSA
|| arch == LLM_ARCH_KIMI_LINEAR
@@ -179,7 +189,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, 1e-5f);
ms.add_kv(LLM_KV_ATTENTION_GROUPNORM_EPS, 1e-5f);
ms.add_kv(LLM_KV_ATTENTION_GROUPNORM_GROUPS, uint32_t(8));
ms.add_kv(LLM_KV_ATTENTION_Q_LORA_RANK, uint32_t(512));
ms.add_kv(LLM_KV_ATTENTION_Q_LORA_RANK, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(64) : uint32_t(512));
ms.add_kv(LLM_KV_ATTENTION_KV_LORA_RANK, uint32_t(512));
ms.add_kv(LLM_KV_ATTENTION_RELATIVE_BUCKETS_COUNT, uint32_t(8));
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW, n_ctx/8);
@@ -205,12 +215,26 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
// MSA requires one indexer head per GQA (KV) head, unlike the DSA archs where the
// indexer head count is independent of the main attention head count.
ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 ? n_head : uint32_t(1));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 || arch == LLM_ARCH_DEEPSEEK4 ? n_head : uint32_t(1));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_TOP_K, uint32_t(8));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_BLOCK_SIZE, uint32_t(4));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_LOCAL_BLOCKS, uint32_t(1));
ms.add_kv(LLM_KV_ROPE_DIMENSION_SECTIONS, std::vector<uint32_t>({n_embd_head/4, n_embd_head/4, n_embd_head/4, n_embd_head/4}));
if (arch == LLM_ARCH_DEEPSEEK4) {
ms.add_kv(LLM_KV_ATTENTION_OUTPUT_GROUP_COUNT, uint32_t(8));
ms.add_kv(LLM_KV_ATTENTION_OUTPUT_LORA_RANK, uint32_t(32));
ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector<uint32_t>({0, 0, 4, 128}));
ms.add_kv(LLM_KV_ATTENTION_COMPRESS_ROPE_FREQ_BASE, 160000.0f);
ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4));
ms.add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, uint32_t(2));
ms.add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, 1.0e-6f);
ms.add_kv(LLM_KV_HASH_LAYER_COUNT, uint32_t(0));
ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 10.0f);
ms.add_kv(LLM_KV_EXPERT_WEIGHTS_SCALE, 1.0f);
ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true);
}
ms.add_kv(LLM_KV_TOKENIZER_MODEL, "no_vocab");
// ms.add_kv(LLM_KV_DENSE_2_FEAT_OUT, n_embd);
// ms.add_kv(LLM_KV_DENSE_3_FEAT_IN, n_embd);
@@ -222,7 +246,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_EXPERT_COUNT, uint32_t(2));
ms.add_kv(LLM_KV_EXPERT_USED_COUNT, uint32_t(1));
ms.add_kv(LLM_KV_EXPERT_SHARED_COUNT, uint32_t(1));
ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, uint32_t(2)); // sigmoid
ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, arch == LLM_ARCH_DEEPSEEK4 ? uint32_t(4) : uint32_t(2));
ms.add_kv(LLM_KV_EXPERT_GROUP_SCALE, 1.0f);
ms.add_kv(LLM_KV_EXPERTS_PER_GROUP, uint32_t(1));
}
@@ -348,6 +372,7 @@ static bool moe_mandatory(const llm_arch arch) {
case LLM_ARCH_DEEPSEEK:
case LLM_ARCH_DEEPSEEK2:
case LLM_ARCH_DEEPSEEK32:
case LLM_ARCH_DEEPSEEK4:
case LLM_ARCH_GLM4_MOE:
case LLM_ARCH_GLM_DSA:
case LLM_ARCH_EXAONE_MOE:
@@ -430,10 +455,6 @@ static bool arch_supported(const llm_arch arch) {
if (arch == LLM_ARCH_DEEPSEEK2OCR) {
return false;
}
if (arch == LLM_ARCH_DEEPSEEK4) {
return false;
}
// FIXME: these hit scheduler/view-backed-output issues with WebGPU on CI.
#ifdef GGML_USE_WEBGPU
if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA) {
@@ -618,10 +639,18 @@ static int test_backends(const llm_arch target_arch, const size_t seed, const gg
if (logits_cpu.empty()) {
model_and_ctx_cpu = get_model_and_ctx(gguf_ctx.get(), nullptr, seed, {}, LLAMA_SPLIT_MODE_LAYER, encode);
logits_cpu = get_logits(model_and_ctx_cpu.first.get(), model_and_ctx_cpu.second.get(), tokens, encode);
if (arch == LLM_ARCH_DEEPSEEK4) {
GGML_ASSERT(llama_memory_seq_rm(
llama_get_memory(model_and_ctx_cpu.second.get()), 0, -1, -1));
}
}
if (dc.split_mode != LLAMA_SPLIT_MODE_TENSOR || llm_arch_supports_sm_tensor(arch)) {
model_and_ctx_dev = get_model_and_ctx(gguf_ctx.get(), nullptr, seed, dc.devs, dc.split_mode, encode);
logits_dev = get_logits(model_and_ctx_dev.first.get(), model_and_ctx_dev.second.get(), tokens, encode);
if (arch == LLM_ARCH_DEEPSEEK4) {
GGML_ASSERT(llama_memory_seq_rm(
llama_get_memory(model_and_ctx_dev.second.get()), 0, -1, -1));
}
const double nmse_val = nmse(logits_cpu, logits_dev);
snprintf(nmse_str, sizeof(nmse_str), "(%.2e)", nmse_val);
status_nmse = "\033[1;32mOK\033[0m";
@@ -634,7 +663,9 @@ static int test_backends(const llm_arch target_arch, const size_t seed, const gg
FILE * file = tmpfile(); // Can be null on Windows without administrator privileges.
// FIXME: when adding a tensor to a gguf_context a copy is made, this changes the pointer which the meta backend
// in turn uses to map the tensors to their simple equivalents - this is fundamentally incompatible
if (file != nullptr && llama_model_saver_supports_arch(arch) && dc.split_mode != LLAMA_SPLIT_MODE_TENSOR) {
// FIXME: DSV4 metadata is not implemented by llama_model_saver.
const bool can_roundtrip = llama_model_saver_supports_arch(arch) && arch != LLM_ARCH_DEEPSEEK4;
if (file != nullptr && can_roundtrip && dc.split_mode != LLAMA_SPLIT_MODE_TENSOR) {
GGML_ASSERT(model_and_ctx_dev.first && model_and_ctx_dev.second);
llama_model_saver ms = llama_model_saver(model_and_ctx_dev.first.get());
ms.add_kv_from_model();
-44
View File
@@ -160,47 +160,6 @@ int llama_completion(int argc, char ** argv) {
// start measuring performance timings from here
llama_perf_context_reset(ctx);
LOG_INF("%s: llama threadpool init, n_threads = %d\n", __func__, (int) params.cpuparams.n_threads);
auto * cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
if (!cpu_dev) {
LOG_ERR("%s: no CPU backend found\n", __func__);
return 1;
}
auto * reg = ggml_backend_dev_backend_reg(cpu_dev);
auto * ggml_threadpool_new_fn = (decltype(ggml_threadpool_new) *) ggml_backend_reg_get_proc_address(reg, "ggml_threadpool_new");
auto * ggml_threadpool_free_fn = (decltype(ggml_threadpool_free) *) ggml_backend_reg_get_proc_address(reg, "ggml_threadpool_free");
struct ggml_threadpool_params tpp_batch =
ggml_threadpool_params_from_cpu_params(params.cpuparams_batch);
struct ggml_threadpool_params tpp =
ggml_threadpool_params_from_cpu_params(params.cpuparams);
if (!set_process_priority(params.cpuparams.priority)) {
LOG_ERR("%s: error: failed to set process priority\n", __func__);
return 1;
}
struct ggml_threadpool * threadpool_batch = NULL;
if (!ggml_threadpool_params_match(&tpp, &tpp_batch)) {
threadpool_batch = ggml_threadpool_new_fn(&tpp_batch);
if (!threadpool_batch) {
LOG_ERR("%s: batch threadpool create failed : n_threads %d\n", __func__, tpp_batch.n_threads);
return 1;
}
// start the non-batch threadpool in the paused state
tpp.paused = true;
}
struct ggml_threadpool * threadpool = ggml_threadpool_new_fn(&tpp);
if (!threadpool) {
LOG_ERR("%s: threadpool create failed : n_threads %d\n", __func__, tpp.n_threads);
return 1;
}
llama_attach_threadpool(ctx, threadpool, threadpool_batch);
const int n_ctx_train = llama_model_n_ctx_train(model);
const int n_ctx = llama_n_ctx(ctx);
@@ -993,8 +952,5 @@ int llama_completion(int argc, char ** argv) {
llama_backend_free();
ggml_threadpool_free_fn(threadpool);
ggml_threadpool_free_fn(threadpool_batch);
return 0;
}
+127 -102
View File
@@ -688,97 +688,99 @@ struct server_slot {
other.prompt = prompt.clone();
other.init_sampler();
}
// returns 0 on success
// caller need to update prompt.tokens after a successful call to keep track of the processing progress
int process_mtmd_chunk(size_t idx, size_t & n_tokens_out) {
GGML_ASSERT(mctx);
const auto & input_tokens = task->tokens;
const auto & chunk = input_tokens.find_chunk(idx);
int32_t res = 0;
auto try_decode = [&]() -> int32_t {
if (mbatch) {
float * embd = mtmd_batch_get_output_embd(mbatch.get(), chunk.get());
if (embd) {
void * cb_data = spec;
static auto cb = [](llama_batch batch, void * user_data) {
common_speculative * spec = static_cast<common_speculative *>(user_data);
if (!common_speculative_process(spec, batch)) {
return 1;
}
return 0;
};
llama_pos new_n_past; // unused for now
res = mtmd_helper_decode_image_chunk(
mctx,
ctx_tgt,
chunk.get(),
embd,
prompt.tokens.pos_next(),
id,
llama_n_batch(ctx_tgt),
&new_n_past,
cb,
cb_data
);
if (res != 0) {
SLT_ERR(*this, "failed to decode mtmd chunk, idx = %zu, res = %d\n", idx, res);
return -1;
}
n_tokens_out = mtmd_input_chunk_get_n_tokens(chunk.get());
return 0; // success
}
}
return 1; // (non-error) need to create & encode batch
};
// if the batch is already exist, try searching & encode
res = try_decode();
if (res == 0) {
return 0;
}
if (res < 0) {
// fatal error
return res;
}
// otherwise, the batch is either uninitialized or is used up
// we need to create & encode a new batch
mbatch.reset(mtmd_batch_init(mctx));
res = mtmd_batch_add_chunk(mbatch.get(), chunk.get());
GGML_ASSERT(res == 0); // we should never have an empty batch
// try batching as much as possible
int n_added = 1;
size_t idx_cur = idx;
while (res == 0) {
auto [next_chunk, next_idx] = input_tokens.find_next_media_chunk(idx_cur);
if (next_chunk == nullptr) {
break;
}
res = mtmd_batch_add_chunk(mbatch.get(), next_chunk->get());
n_added += (res == 0 ? 1 : 0);
idx_cur = next_idx;
SLT_DBG(*this, "try adding media chunk idx = %zu to batch, res = %d\n", next_idx, res);
// if res != 0, batch is full or chunk is not compatible -> this loop breaks
}
// TODO @ngxson : move this log line to debug when it become more stable
SLT_TRC(*this, "encoding mtmd batch from idx = %zu, n_chunks = %d\n", idx, n_added);
res = mtmd_batch_encode(mbatch.get());
if (res != 0) {
SLT_ERR(*this, "failed to encode mtmd batch for chunk idx = %zu, res = %d\n", idx, res);
return -1;
}
return try_decode();
}
};
// returns 0 on success
// caller need to update prompt.tokens after a successful call to keep track of the processing progress
// note: this is not a member of server_slot because we want to run it inside yield_to_queue
// slot is passed as const to avoid accidental modification of the slot state
// some pointers are allowed to be used, they are not used by to_json()
static int process_mtmd_chunk(const server_slot & slot, mtmd::batch_ptr & mbatch, size_t idx, size_t & n_tokens_out) {
GGML_ASSERT(slot.mctx);
const auto & mctx = slot.mctx;
const auto & input_tokens = slot.task->tokens;
const auto & chunk = input_tokens.find_chunk(idx);
int32_t res = 0;
auto try_decode = [&]() -> int32_t {
if (mbatch) {
float * embd = mtmd_batch_get_output_embd(mbatch.get(), chunk.get());
if (embd) {
void * cb_data = slot.spec;
static auto cb = [](llama_batch batch, void * user_data) {
common_speculative * spec = static_cast<common_speculative *>(user_data);
if (!common_speculative_process(spec, batch)) {
return 1;
}
return 0;
};
llama_pos new_n_past; // unused for now
res = mtmd_helper_decode_image_chunk(
mctx,
slot.ctx_tgt,
chunk.get(),
embd,
slot.prompt.tokens.pos_next(),
slot.id,
llama_n_batch(slot.ctx_tgt),
&new_n_past,
cb,
cb_data
);
if (res != 0) {
SLT_ERR(slot, "failed to decode mtmd chunk, idx = %zu, res = %d\n", idx, res);
return -1;
}
n_tokens_out = mtmd_input_chunk_get_n_tokens(chunk.get());
return 0; // success
}
}
return 1; // (non-error) need to create & encode batch
};
// if the batch is already exist, try searching & encode
res = try_decode();
if (res == 0) {
return 0;
}
if (res < 0) {
// fatal error
return res;
}
// otherwise, the batch is either uninitialized or is used up
// we need to create & encode a new batch
mbatch.reset(mtmd_batch_init(mctx));
res = mtmd_batch_add_chunk(mbatch.get(), chunk.get());
GGML_ASSERT(res == 0); // we should never have an empty batch
// try batching as much as possible
int n_added = 1;
size_t idx_cur = idx;
while (res == 0) {
auto [next_chunk, next_idx] = input_tokens.find_next_media_chunk(idx_cur);
if (next_chunk == nullptr) {
break;
}
res = mtmd_batch_add_chunk(mbatch.get(), next_chunk->get());
n_added += (res == 0 ? 1 : 0);
idx_cur = next_idx;
SLT_DBG(slot, "try adding media chunk idx = %zu to batch, res = %d\n", next_idx, res);
// if res != 0, batch is full or chunk is not compatible -> this loop breaks
}
// TODO @ngxson : move this log line to debug when it become more stable
SLT_TRC(slot, "encoding mtmd batch from idx = %zu, n_chunks = %d\n", idx, n_added);
res = mtmd_batch_encode(mbatch.get());
if (res != 0) {
SLT_ERR(slot, "failed to encode mtmd batch for chunk idx = %zu, res = %d\n", idx, res);
return -1;
}
return try_decode();
}
//
// server_context_impl (private implementation)
@@ -1354,8 +1356,8 @@ private:
GGML_ASSERT(!sleeping);
// wiring up server queues
queue_tasks.on_new_task([this](server_task && task) {
process_single_task(std::move(task));
queue_tasks.on_new_task([this](server_task && task, bool is_yielding) {
return process_single_task(std::move(task), is_yielding);
});
queue_tasks.on_update_slots([this]() {
update_slots();
@@ -2286,7 +2288,14 @@ private:
cur.pos_max, cur.n_tokens, (float) cur.size() / 1024 / 1024);
}
void process_single_task(server_task && task) {
// returns false to decline the task, it is offered again after the decode is done
bool process_single_task(server_task && task, bool is_yielding) {
// while yielding, an encode / decode is running and only accessing metrics is safe
if (is_yielding && task.type != SERVER_TASK_TYPE_METRICS) {
SRV_DBG("decoding, decline task, id_task = %d\n", task.id);
return false;
}
switch (task.type) {
case SERVER_TASK_TYPE_COMPLETION:
case SERVER_TASK_TYPE_INFILL:
@@ -2620,6 +2629,8 @@ private:
queue_results.send(std::move(res));
} break;
}
return true;
}
void iterate(std::vector<server_slot> & slots, std::function<void(server_slot &)> callback) {
@@ -3382,8 +3393,13 @@ private:
// so the timing is queued and flushed on the next sync
metrics_pre_decode();
// encode on the worker thread, so we can still handle metrics tasks
size_t n_tokens_out = 0;
int32_t res = slot.process_mtmd_chunk(cur_token_idx, n_tokens_out);
int32_t res = 0;
queue_tasks.yield_to_queue([&]() {
res = process_mtmd_chunk(slot, slot.mbatch, cur_token_idx, n_tokens_out);
});
if (res != 0) {
SLT_ERR(slot, "failed to process mtmd chunk, res = %d\n", res);
send_error(slot, "failed to process mtmd chunk", ERROR_TYPE_SERVER);
@@ -3557,7 +3573,20 @@ private:
}
}
const int ret = llama_decode(ctx_tgt, batch_view);
bool has_output = false;
for (int i = off; i < off + batch_view.n_tokens; ++i) {
has_output |= batch.tokens[i].output;
}
// decode on the worker thread, so we can still handle metrics tasks while waiting
// note: the sync is done here too, so that the wait also happens off the main thread
int ret = 0;
queue_tasks.yield_to_queue([&]() {
ret = llama_decode(ctx_tgt, batch_view);
if (ret == 0 && has_output) {
llama_synchronize(ctx_tgt);
}
});
if (ret != 0) {
{
@@ -3609,7 +3638,7 @@ private:
return false; // retry with the updated n_batch
} else {
// success, apply batch metrics
metrics_post_decode(off, batch_view.n_tokens);
metrics_post_decode(off, batch_view.n_tokens, has_output);
}
// TODO: avoid restoring the draft context and re-evaluating the drafted tokens when not needed [TAG_SPEC_AVOID_DRAFT_REEVAL]
@@ -3922,7 +3951,8 @@ private:
n_prompt_queued = 0;
}
void metrics_post_decode(int32_t off, int32_t n_tokens) {
// has_output is computed by the caller, which also already synchronized the context if it is set
void metrics_post_decode(int32_t off, int32_t n_tokens, bool has_output) {
metrics.n_decode++;
for (const auto & slot : slots) {
if (slot.is_processing()) {
@@ -3935,13 +3965,10 @@ private:
// note: a slot can be released before we get here, which clears its stats
// the tokens were still computed, counted in the global metrics, not in slot
uint64_t n_prompt_tokens = 0;
bool has_output = false;
for (int i = off; i < off + n_tokens; ++i) {
const auto & t = batch.tokens[i];
has_output |= t.output;
if (!t.is_prompt) {
continue; // generated tokens are handled after sampling
}
@@ -3957,14 +3984,12 @@ private:
metrics_queue_prompt(n_prompt_tokens);
if (has_output) {
// sync if we have at least one output in batch
// so that we can calculate the timings correctly
llama_synchronize(ctx_tgt);
// the context is already synchronized, so the timings are correct
metrics_flush_prompt();
}
// advance the prompt timing of the slots that had tokens in this batch
// note: a second pass, it must run after the sync above to reflect the compute
// note: a second pass, it must run after the sync to reflect the compute
const int64_t t_now = ggml_time_us();
for (int i = off; i < off + n_tokens; ++i) {
const auto & t = batch.tokens[i];
+137 -19
View File
@@ -4,6 +4,7 @@
#include "log.h"
#include <chrono>
#include <thread>
#define QUE_INF(fmt, ...) LOG_INF("que %12.*s: " fmt, 12, __func__, __VA_ARGS__)
#define QUE_WRN(fmt, ...) LOG_WRN("que %12.*s: " fmt, 12, __func__, __VA_ARGS__)
@@ -122,10 +123,135 @@ void server_queue::terminate() {
condition_tasks.notify_all();
}
bool server_queue::process_new_tasks(bool is_yielding) {
while (true) {
std::unique_lock<std::mutex> lock(mutex_tasks);
if (!running) {
QUE_DBG("%s", "terminate\n");
return true;
}
if (queue_tasks.empty()) {
return false;
}
server_task task = std::move(queue_tasks.front());
queue_tasks.pop_front();
lock.unlock();
QUE_DBG("processing task, id = %d\n", task.id);
if (!callback_new_task(std::move(task), is_yielding)) {
// set it aside, do not put it back in the queue, else we offer it again in a loop
GGML_ASSERT(is_yielding && "a task can only be declined while yielding");
QUE_DBG("task declined, id = %d\n", task.id);
lock.lock();
queue_tasks_unhandled.push_back(std::move(task));
}
}
}
void server_queue::worker_loop() {
while (true) {
std::function<void()> work;
{
std::unique_lock<std::mutex> lock(mutex_tasks);
worker.cv.wait(lock, [&]{
return worker.stop || worker.work != nullptr;
});
if (worker.stop) {
return;
}
work = std::move(worker.work);
worker.work = nullptr;
}
// note: do not hold any lock here, work() may post new tasks
std::exception_ptr exception;
try {
work();
} catch (...) {
exception = std::current_exception();
}
// signal completion to yield_to_queue()
std::unique_lock<std::mutex> lock(mutex_tasks);
worker.exception = std::move(exception);
worker.busy = false;
condition_tasks.notify_all();
}
}
void server_queue::worker_stop() {
if (!worker.thread.joinable()) {
return;
}
{
std::unique_lock<std::mutex> lock(mutex_tasks);
worker.stop = true;
}
worker.cv.notify_one();
worker.thread.join();
}
void server_queue::yield_to_queue(std::function<void()> && work) {
GGML_ASSERT(worker.thread.joinable() && "yield_to_queue() requires start_loop() to be running");
QUE_DBG("%s", "yielding to queue\n");
{
std::unique_lock<std::mutex> lock(mutex_tasks);
GGML_ASSERT(!worker.busy && "yield_to_queue() cannot be nested");
worker.busy = true;
worker.work = std::move(work);
}
worker.cv.notify_one();
while (true) {
// note: on terminate this is a no-op, but we still wait for the work to finish
process_new_tasks(true);
std::unique_lock<std::mutex> lock(mutex_tasks);
// declined tasks are moved to queue_tasks_unhandled, so a non-empty queue always has something new
condition_tasks.wait(lock, [&]{
return !worker.busy || (running && !queue_tasks.empty());
});
if (!worker.busy) {
break;
}
}
std::exception_ptr exception;
{
std::unique_lock<std::mutex> lock(mutex_tasks);
// put the declined tasks back, keeping their order
while (!queue_tasks_unhandled.empty()) {
queue_tasks.push_front(std::move(queue_tasks_unhandled.back()));
queue_tasks_unhandled.pop_back();
}
// make sure to avoid idle timeout here
time_last_task = ggml_time_ms();
// the worker is idle now, take the exception it may have left behind
std::swap(exception, worker.exception);
}
QUE_DBG("%s", "done yielding to queue\n");
// note: rethrow only after the declined tasks are back in the queue, so they are not lost
if (exception) {
std::rethrow_exception(exception);
}
}
void server_queue::start_loop(int64_t idle_sleep_ms) {
running = true;
time_last_task = ggml_time_ms();
// spawn the worker thread used by yield_to_queue()
GGML_ASSERT(!worker.thread.joinable() && "start_loop() is already running");
worker.stop = false;
worker.thread = std::thread([this]() { worker_loop(); });
constexpr auto max_wait_time = std::chrono::seconds(1);
auto should_sleep = [&]() -> bool {
// caller must hold mutex_tasks
@@ -138,24 +264,10 @@ void server_queue::start_loop(int64_t idle_sleep_ms) {
while (true) {
QUE_DBG("%s", "processing new tasks\n");
while (true) {
std::unique_lock<std::mutex> lock(mutex_tasks);
if (!running) {
QUE_DBG("%s", "terminate\n");
return;
}
if (queue_tasks.empty()) {
lock.unlock();
break;
}
server_task task = std::move(queue_tasks.front());
queue_tasks.pop_front();
lock.unlock();
QUE_DBG("processing task, id = %d\n", task.id);
callback_new_task(std::move(task));
if (process_new_tasks(false)) {
break; // terminate
}
// all tasks in the current loop is processed, slots data is now ready
QUE_DBG("%s", "update slots\n");
@@ -206,6 +318,8 @@ void server_queue::start_loop(int64_t idle_sleep_ms) {
}
}
}
worker_stop();
}
void server_queue::cleanup_pending_task(int id_target) {
@@ -214,11 +328,15 @@ void server_queue::cleanup_pending_task(int id_target) {
return task.id == id_target;
};
queue_tasks.erase(
std::remove_if(queue_tasks.begin(), queue_tasks.end(), rm_func),
std::remove_if(queue_tasks.begin(), queue_tasks.end(), rm_func),
queue_tasks.end());
queue_tasks_deferred.erase(
std::remove_if(queue_tasks_deferred.begin(), queue_tasks_deferred.end(), rm_func),
std::remove_if(queue_tasks_deferred.begin(), queue_tasks_deferred.end(), rm_func),
queue_tasks_deferred.end());
// a task declined while yielding is not in queue_tasks yet, but it can still be cancelled
queue_tasks_unhandled.erase(
std::remove_if(queue_tasks_unhandled.begin(), queue_tasks_unhandled.end(), rm_func),
queue_tasks_unhandled.end());
}
//
+43 -4
View File
@@ -4,7 +4,9 @@
#include <condition_variable>
#include <deque>
#include <exception>
#include <mutex>
#include <thread>
#include <vector>
#include <unordered_set>
@@ -21,16 +23,32 @@ private:
// queues
std::deque<server_task> queue_tasks;
std::deque<server_task> queue_tasks_deferred;
// tasks declined while yielding, put back in queue_tasks once the yield is done
// note: kept as a member so that cleanup_pending_task() can also reach them
std::deque<server_task> queue_tasks_unhandled;
std::mutex mutex_tasks;
std::condition_variable condition_tasks;
// used by yield_to_queue, all fields are guarded by mutex_tasks
struct worker_t {
std::thread thread;
std::condition_variable cv; // the worker sleeps on this until there is work
std::function<void()> work; // pending work, picked up by the thread
std::exception_ptr exception; // exception thrown by work(), if any
bool stop = false;
bool busy = false;
};
worker_t worker;
// callback functions
std::function<void(server_task &&)> callback_new_task;
std::function<void(void)> callback_update_slots;
std::function<void(bool)> callback_sleeping_state;
std::function<bool(server_task &&, bool)> callback_new_task;
std::function<void(void)> callback_update_slots;
std::function<void(bool)> callback_sleeping_state;
public:
~server_queue() { worker_stop(); }
// Add a new task to the end of the queue
int post(server_task && task, bool front = false);
@@ -75,6 +93,15 @@ public:
*/
void start_loop(int64_t idle_sleep_ms = -1);
// run work() on a separate thread, while the current thread calls process_new_tasks
// returns once work() is done (may throw exceptions)
// must be called from start_loop() thread (ideally inside callback_update_slots)
// use case: return metrics while encode/decode is running
// ref: https://github.com/ggml-org/llama.cpp/pull/27041
//
// tasks declined by callback_new_task are put back in the queue once this returns
void yield_to_queue(std::function<void()> && work);
// for metrics
size_t queue_tasks_deferred_size() {
std::unique_lock<std::mutex> lock(mutex_tasks);
@@ -86,7 +113,10 @@ public:
//
// Register function to process a new task
void on_new_task(std::function<void(server_task &&)> callback) {
// the second argument tells whether the queue is currently yielding (see yield_to_queue)
// only then may the callback return false to decline the task, and it must leave it
// untouched, so that it can be put back in the queue later
void on_new_task(std::function<bool(server_task &&, bool)> callback) {
callback_new_task = std::move(callback);
}
@@ -112,6 +142,15 @@ public:
private:
void cleanup_pending_task(int id_target);
// process all pending tasks in the queue
// returns true if the queue is terminated, false if there is no more task to process
// while yielding, declined tasks are moved to queue_tasks_unhandled
bool process_new_tasks(bool is_yielding);
// for worker_t
void worker_loop();
void worker_stop();
};
// struct for managing server responses
@@ -5,6 +5,7 @@
ChatAttachmentsPreviewNavButtons,
ChatAttachmentsPreviewThumbnailStrip
} from '$lib/components/app';
import { UI_DATA_ATTRS } from '$lib/constants';
import { modelsStore } from '$lib/stores';
import {
createBase64DataUrl,
@@ -90,7 +91,7 @@
const index = currentIndex;
setTimeout(() => {
const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
const thumbnail = document.querySelector(`[${UI_DATA_ATTRS.THUMBNAIL_INDEX}="${index}"]`);
thumbnail?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}, 0);
@@ -1,7 +1,7 @@
<script lang="ts">
import { FileText, Music, Video } from '@lucide/svelte';
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ICON_CLASS_DEFAULT, UI_DATA_ATTRS } from '$lib/constants';
interface PreviewItem {
id: string;
@@ -36,7 +36,7 @@
<HorizontalScrollCarousel class="max-w-full">
{#each items as item, index (item.id)}
<button
data-thumbnail-index={index}
{...{ [UI_DATA_ATTRS.THUMBNAIL_INDEX]: index }}
class={[
'relative flex-shrink-0 cursor-pointer overflow-hidden rounded border-2 bg-black/80 backdrop-blur-sm transition-all hover:opacity-90',
index === currentIndex ? 'border-white' : 'border-transparent opacity-60',
@@ -3,12 +3,11 @@
import {
ChatAttachmentsList,
ChatFormActions,
ChatFormContentEditable,
ChatFormFileInputInvisible,
ChatFormCurrentWorkingDirectory,
ChatFormInput,
ChatFormInputFileInputInvisible,
ChatFormMcpResourcesList,
ChatFormPickers,
ChatFormTextarea,
ChatFormWorkingDirectory,
DialogMcpResourcesBrowser
} from '$lib/components/app';
import {
@@ -110,7 +109,7 @@
}: Props = $props();
// Component References
// Shared handle of the two input renderers (textarea + contenteditable).
// Shared handle of the two input renderers (plain textarea + rich chat form input).
type ChatInputHandle = {
focus(): void;
resetHeight(): void;
@@ -121,16 +120,16 @@
let audioRecorder: AudioRecorder | undefined;
let chatFormActionsRef: ChatFormActions | undefined = $state(undefined);
let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined);
let fileInputRef: ChatFormInputFileInputInvisible | undefined = $state(undefined);
let pickersRef: { handleKeydown: (event: KeyboardEvent) => boolean } | undefined =
$state(undefined);
let inputRef: ChatInputHandle | undefined = $state(undefined);
// Render-mode gate: the plain textarea by default, the contenteditable
// Render-mode gate: the plain textarea by default, the rich chat form input
// while the buffer carries a `file://` mention link or a complete code
// span (badges and code chips need a DOM the textarea cannot provide).
// Demotes back once neither remains.
let useContenteditable = $state(false);
let useRichInput = $state(false);
// Audio Recording State
let isRecording = $state(false);
@@ -242,16 +241,15 @@
}
$effect(() => {
const wantContenteditable =
containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
const wantRichInput = containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
if (useContenteditable === wantContenteditable) return;
if (useRichInput === wantRichInput) return;
if (!caretOffsetPinned) {
pendingCaretOffset = inputRef?.getCaretOffset() ?? (value ?? '').length;
}
useContenteditable = wantContenteditable;
useRichInput = wantRichInput;
queueCaretRestore();
});
@@ -315,7 +313,7 @@
// Caret inside a fenced code block (closed, or still open
// while being typed): Enter adds a line, never submits. The
// contenteditable consumes this case locally; this gate
// rich chat form input consumes this case locally; this gate
// covers the plain textarea, where skipping submit lets the
// native newline through.
if (!isModifier && isOffsetInCodeBlock(value ?? '', inputRef?.getCaretOffset() ?? 0)) {
@@ -508,9 +506,9 @@
value = built.newValue;
onValueChange?.(built.newValue);
// Already in contenteditable mode: no renderer flip, so the swap
// Already in rich chat form input mode: no renderer flip, so the swap
// effect's caret restore never runs.
if (useContenteditable) {
if (useRichInput) {
queueCaretRestore();
}
}
@@ -544,7 +542,7 @@
}
</script>
<ChatFormFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
<ChatFormInputFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
<form
class="relative grid {className}"
@@ -603,35 +601,20 @@
<div
class="flex-column relative min-h-12 items-center rounded-4xl md:rounded-3xl py-2 pb-2.25 shadow-sm transition-all focus-within:shadow-md md:py-3!"
>
{#if useContenteditable}
<ChatFormContentEditable
class="px-5 py-1.5 md:pt-0 mb-0.5"
bind:this={inputRef}
bind:value
onKeydown={handleKeydown}
onInput={() => {
pickers.handleInput();
onValueChange?.(value);
}}
onPaste={handlePaste}
{disabled}
{placeholder}
/>
{:else}
<ChatFormTextarea
class="px-5 py-1.5 md:pt-0"
bind:this={inputRef}
bind:value
onKeydown={handleKeydown}
onInput={() => {
pickers.handleInput();
onValueChange?.(value);
}}
onPaste={handlePaste}
{disabled}
{placeholder}
/>
{/if}
<ChatFormInput
class="px-5 py-1.5 md:pt-0"
bind:this={inputRef}
bind:value
onKeydown={handleKeydown}
onInput={() => {
pickers.handleInput();
onValueChange?.(value);
}}
onPaste={handlePaste}
{disabled}
{placeholder}
{useRichInput}
/>
{#if mcpResourceStore.hasAttachments}
<ChatFormMcpResourcesList
@@ -667,7 +650,7 @@
<ContextGaugePopup />
{#if toolsStore.hasEnabledCwdTools}
<ChatFormWorkingDirectory
<ChatFormCurrentWorkingDirectory
directory={cwd}
isOpen={pickers.isWorkingDirectoryPickerOpen}
bind:query={pickers.workingDirectoryQuery}
@@ -1,10 +1,10 @@
<script lang="ts">
import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte';
import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte';
import ChatFormCurrentWorkingDirectoryChip from './ChatFormCurrentWorkingDirectoryChip.svelte';
import ChatFormCurrentWorkingDirectoryResultsList from './ChatFormCurrentWorkingDirectoryResultsList.svelte';
import { FolderOpen } from '@lucide/svelte';
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
import * as Popover from '$lib/components/ui/popover';
import { DEFAULT_MOBILE_BREAKPOINT, HOME_TILDE, SEARCH } from '$lib/constants';
import { DEFAULT_MOBILE_BREAKPOINT, HOME_TILDE, SEARCH, UI_DATA_ATTRS } from '$lib/constants';
import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
@@ -120,7 +120,7 @@
});
useScrollActiveRow({
dataIndex: 'result',
dataAttr: UI_DATA_ATTRS.RESULT_INDEX,
getContainer: () => listContainer,
getCount: () => queryResults.length,
getIndex: () => nav.hoveredIndex,
@@ -250,7 +250,7 @@
// user cancelled - silently ignore; other errors are logged
if (err instanceof DOMException && err.name === 'AbortError') return;
console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err);
console.error('[ChatFormCurrentWorkingDirectory] showDirectoryPicker failed:', err);
}
}
@@ -331,7 +331,7 @@
onclick={onOpen}
{disabled}
>
<ChatFormWorkingDirectoryChip
<ChatFormCurrentWorkingDirectoryChip
{directory}
{homeBase}
{disabled}
@@ -372,7 +372,7 @@
{#if !fileSearchEnabled}
<div class="px-2 py-1.5 text-sm text-muted-foreground">{searchUnavailableMessage}</div>
{:else if query.trim() && (search.isSearching || queryResults.length > 0 || searchError)}
<ChatFormWorkingDirectoryResultsList
<ChatFormCurrentWorkingDirectoryResultsList
results={queryResults}
hoveredIndex={nav.hoveredIndex}
isSearching={search.isSearching}
@@ -1,6 +1,7 @@
<script lang="ts">
import { Folder } from '@lucide/svelte';
import { cn } from '$lib/components/ui/utils';
import { UI_DATA_ATTRS } from '$lib/constants';
import { highlightMatch } from '$lib/utils';
import { fly } from 'svelte/transition';
@@ -46,7 +47,7 @@
{#each results as path, index (path)}
<button
type="button"
data-result-index={index}
{...{ [UI_DATA_ATTRS.RESULT_INDEX]: index }}
data-highlighted={index === hoveredIndex ? '' : undefined}
class={cn(
'relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground'
@@ -0,0 +1,78 @@
<script lang="ts">
import ChatFormInputBasic from './ChatFormInputBasic.svelte';
import ChatFormInputRich from './ChatFormInputRich.svelte';
interface Props {
class?: string;
disabled?: boolean;
onInput?: () => void;
onKeydown?: (event: KeyboardEvent) => void;
onPaste?: (event: ClipboardEvent) => void;
placeholder?: string;
value?: string;
useRichInput?: boolean;
}
let {
class: className = '',
disabled = false,
onInput,
onKeydown,
onPaste,
placeholder = 'Ask anything...',
useRichInput = false,
value = $bindable('')
}: Props = $props();
let basicRef: ChatFormInputBasic | undefined = $state();
let richRef: ChatFormInputRich | undefined = $state();
// The two renderers share one imperative handle (focus/caret/height), so
// the parent can drive whichever variant is mounted through this one.
export function getElement() {
return useRichInput ? richRef?.getElement() : basicRef?.getElement();
}
export function focus() {
if (useRichInput) richRef?.focus();
else basicRef?.focus();
}
export function resetHeight() {
if (useRichInput) richRef?.resetHeight();
else basicRef?.resetHeight();
}
export function getCaretOffset(): number {
return useRichInput ? (richRef?.getCaretOffset() ?? 0) : (basicRef?.getCaretOffset() ?? 0);
}
export function setCaretOffset(offset: number) {
if (useRichInput) richRef?.setCaretOffset(offset);
else basicRef?.setCaretOffset(offset);
}
</script>
{#if useRichInput}
<ChatFormInputRich
bind:this={richRef}
class={className}
{disabled}
{onInput}
{onKeydown}
{onPaste}
{placeholder}
bind:value
/>
{:else}
<ChatFormInputBasic
bind:this={basicRef}
class={className}
{disabled}
{onInput}
{onKeydown}
{onPaste}
{placeholder}
bind:value
/>
{/if}
@@ -48,7 +48,7 @@
}
}
// Plain-text caret offsets, shared with the contenteditable variant so
// Plain-text caret offsets, shared with the rich chat form input variant so
// the picker/paste flows can address either renderer through one handle.
export function getCaretOffset(): number {
if (!textareaElement) return 0;
@@ -1,8 +1,8 @@
<script lang="ts">
import { CODE_BLOCK } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { CODE_BLOCK, CODE_TOKEN_ATTR, UI_DATA_ATTRS } from '$lib/constants';
import { BooleanString, ChatFormInputRichTokenKind, ColorMode } from '$lib/enums';
import { isMobile } from '$lib/stores';
import type { ContentEditableToken } from '$lib/types';
import type { ChatFormInputRichToken } from '$lib/types';
import type { SourceHistoryEntry } from '$lib/utils';
import {
badgeAwareWordJump,
@@ -53,7 +53,7 @@
// browser's native undo stack.
const history = new SourceHistory();
// Browsers disagree on what an empty contenteditable contains (`<br>`,
// Browsers disagree on what an empty rich chat form input contains (`<br>`,
// `<div><br></div>`, or nothing), so emptiness is decided by the
// serialized source, not the DOM shape.
function syncEmptyState(serialized?: string) {
@@ -61,15 +61,15 @@
const source = serialized ?? serializeContent(rootElement);
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
rootElement.dataset.empty = source.length === 0 ? BooleanString.TRUE : BooleanString.FALSE;
}
function renderTokens(tokens: ContentEditableToken[]) {
function renderTokens(tokens: ChatFormInputRichToken[]) {
if (!rootElement) return;
const caret = rangeToTextOffset(rootElement, safeRange());
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
rootElement.replaceChildren(buildFragment(tokens));
syncCodeBlockHatches(rootElement);
@@ -127,7 +127,9 @@
}
function highlightCodeBlocks(root: HTMLElement) {
for (const el of root.querySelectorAll<HTMLElement>('code[data-code-token="block"]')) {
for (const el of root.querySelectorAll<HTMLElement>(
`code[${CODE_TOKEN_ATTR}="${ChatFormInputRichTokenKind.CODE_BLOCK}"]`
)) {
highlightCodeBlockElement(el);
}
}
@@ -151,7 +153,10 @@
}
while (node && node !== rootElement) {
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
if (
node instanceof HTMLElement &&
node.getAttribute(CODE_TOKEN_ATTR) === ChatFormInputRichTokenKind.CODE_BLOCK
) {
const caret = rangeToTextOffset(rootElement, range);
if (highlightCodeBlockElement(node)) {
@@ -189,11 +194,13 @@
* (deduped via the data attribute) swapped on mode change.
*/
function loadHighlightTheme(isDark: boolean) {
document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove());
document
.querySelectorAll(`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`)
.forEach((s) => s.remove());
const style = document.createElement('style');
style.setAttribute('data-highlight-theme-preview', 'true');
style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
style.textContent = isDark ? githubDarkCss : githubLightCss;
document.head.appendChild(style);
@@ -311,7 +318,7 @@
source[source.length - 2] !== '\n' &&
last?.nodeType === Node.TEXT_NODE
) {
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
rootElement.appendChild(document.createTextNode('\n'));
restoreCaret(source.length);
resizeHeight();
@@ -404,7 +411,10 @@
let node: Node | null = container.parentNode;
while (node && node !== rootElement) {
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
if (
node instanceof HTMLElement &&
node.getAttribute(CODE_TOKEN_ATTR) === ChatFormInputRichTokenKind.CODE_BLOCK
) {
const tail = document.createRange();
tail.setStart(container, offset);
@@ -462,7 +472,11 @@
const first = rootElement.firstChild;
if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false;
if (
!(first instanceof HTMLElement) ||
first.getAttribute(CODE_TOKEN_ATTR) !== ChatFormInputRichTokenKind.CODE_BLOCK
)
return false;
const range = safeRange();
@@ -483,7 +497,7 @@
if (firstLineEnd !== -1 && caret > firstLineEnd) return false;
}
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
rootElement.prepend(document.createElement('br'));
restoreCaret(0, extend);
@@ -507,7 +521,11 @@
const second = first.nextSibling;
if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return;
if (
!(second instanceof HTMLElement) ||
second.getAttribute(CODE_TOKEN_ATTR) !== ChatFormInputRichTokenKind.CODE_BLOCK
)
return;
const range = safeRange();
const onHatch =
@@ -787,7 +805,7 @@
}
</script>
<div class="flex-1 {className}">
<div class="flex-1 {className} mb-0.5">
<div
bind:this={rootElement}
contenteditable={!disabled}
@@ -798,7 +816,7 @@
data-placeholder={placeholder}
tabindex={disabled ? -1 : 0}
class={[
'chat-form-contenteditable text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
'chat-form-input-rich text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
disabled && 'cursor-not-allowed'
]}
style="max-height: var(--max-message-height);"
@@ -815,18 +833,18 @@
<style>
/* pre-wrap is load-bearing: without it Chromium collapses \n in
text nodes and converts them to spaces while typing */
.chat-form-contenteditable {
.chat-form-input-rich {
white-space: pre-wrap;
}
.chat-form-contenteditable:global([data-empty='true'])::before {
.chat-form-input-rich:global([data-empty='true'])::before {
content: attr(data-placeholder);
color: var(--muted-foreground);
pointer-events: none;
}
/* Inline code - mirrors markdown-content.css */
.chat-form-contenteditable :global(code[data-code-token='inline']) {
.chat-form-input-rich :global(code[data-code-token='code_inline']) {
background: var(--muted);
color: var(--muted-foreground);
padding: 0.125rem 0.375rem;
@@ -835,7 +853,7 @@
}
/* Fenced code block - mirrors .code-block-wrapper in markdown-content.css */
.chat-form-contenteditable :global(code[data-code-token='block']) {
.chat-form-input-rich :global(code[data-code-token='code_block']) {
display: block;
margin: 0.25rem 0;
padding: 0.75rem 1rem;
@@ -1,7 +1,7 @@
<script lang="ts" generics="T">
import { SearchInput } from '$lib/components/app';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
import { CHAT_FORM_POPOVER_MAX_HEIGHT, UI_DATA_ATTRS } from '$lib/constants';
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
import type { Snippet } from 'svelte';
@@ -55,7 +55,7 @@
// selectedIndex/items.length are untracked so hover and result replacement
// never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
useScrollActiveRow({
dataIndex: 'picker',
dataAttr: UI_DATA_ATTRS.PICKER_INDEX,
getContainer: () => listContainer,
getCount: () => items.length,
getIndex: () => selectedIndex,
@@ -1,4 +1,5 @@
<script lang="ts">
import { UI_DATA_ATTRS } from '$lib/constants';
import type { Snippet } from 'svelte';
interface Props {
@@ -24,7 +25,7 @@
<button
type="button"
data-picker-index={dataIndex}
{...{ [UI_DATA_ATTRS.PICKER_INDEX]: dataIndex }}
{disabled}
{onclick}
{onmouseenter}
@@ -1,7 +1,7 @@
<script lang="ts">
import ChatFormCommandPicker from './ChatFormCommandPicker.svelte';
import ChatFormMentionPicker from './ChatFormMentionPicker.svelte';
import ChatFormPickerCommand from './ChatFormPickerCommand.svelte';
import ChatFormPickerMcpPrompts from './ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte';
import ChatFormPickerMention from './ChatFormPickerMention.svelte';
import type {
ChatFormCommand,
FileMentionEntry,
@@ -55,9 +55,9 @@
scopePath
}: Props = $props();
let commandPickerRef: ChatFormCommandPicker | undefined = $state(undefined);
let commandPickerRef: ChatFormPickerCommand | undefined = $state(undefined);
let promptPickerRef: ChatFormPickerMcpPrompts | undefined = $state(undefined);
let mentionPickerRef: ChatFormMentionPicker | undefined = $state(undefined);
let mentionPickerRef: ChatFormPickerMention | undefined = $state(undefined);
/** Delegate keyboard events to the active picker child; true if handled. */
export function handleKeydown(event: KeyboardEvent): boolean {
@@ -77,7 +77,7 @@
}
</script>
<ChatFormCommandPicker
<ChatFormPickerCommand
bind:this={commandPickerRef}
isOpen={isCommandPickerOpen ?? false}
query={commandQuery ?? ''}
@@ -96,7 +96,7 @@
{onPromptLoadError}
/>
<ChatFormMentionPicker
<ChatFormPickerMention
bind:this={mentionPickerRef}
isOpen={isMentionPickerOpen ?? false}
query={mentionQuery ?? ''}
+15 -20
View File
@@ -91,7 +91,7 @@ export { default as ChatAttachmentsListItemThumbnailImage } from './ChatAttachme
* preview without carousel, or a gallery/carousel view when multiple items exist.
* Uses ChatAttachmentPreviewSingle internally for each item's content.
*/
export { default as ChatAttachmentsPreview } from './ChatAttachments/ChatAttachmentsPreview.svelte';
export { default as ChatAttachmentsPreview } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreview.svelte';
export { default as ChatAttachmentsPreviewNavButtons } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewNavButtons.svelte';
export { default as ChatAttachmentsPreviewFileInfo } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewFileInfo.svelte';
export { default as ChatAttachmentsPreviewThumbnailStrip } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte';
@@ -120,8 +120,8 @@ export { default as ChatAttachmentsPreviewCurrentItem } from './ChatAttachments/
* Used by ChatScreenForm and ChatMessageEditForm for both new conversations and message editing.
*
* **Architecture:**
* - Composes ChatFormTextarea (or ChatFormContentEditable for messages with
* file mention links), ChatFormActions, and ChatFormPickerMcpPrompts
* - Composes ChatFormInput (a plain textarea, or a ChatFormInputRich for
* messages with file mention links), ChatFormActions, and ChatFormPickerMcpPrompts
* - Manages file upload state via `uploadedFiles` bindable prop
* - Integrates with ModelsSelectorDropdown for model selection in router mode
* - Communicates with parent via callbacks (onSubmit, onFilesAdd, onStop, etc.)
@@ -258,7 +258,7 @@ export { default as ChatFormContextGauge } from './ChatForm/ChatFormContextGauge
/**
* Hidden file input element for programmatic file selection.
*/
export { default as ChatFormFileInputInvisible } from './ChatForm/ChatFormFileInputInvisible.svelte';
export { default as ChatFormInputFileInputInvisible } from './ChatForm/ChatFormInput/ChatFormInputFileInputInvisible.svelte';
/**
* Displays MCP Resource attachments as a horizontal carousel.
@@ -267,18 +267,13 @@ export { default as ChatFormFileInputInvisible } from './ChatForm/ChatFormFileIn
export { default as ChatFormMcpResourcesList } from './ChatForm/ChatFormMcpResourcesList.svelte';
/**
* Auto-resizing contenteditable input that renders `[name](file://...)`
* mention links as inline chips while keeping the value as the markdown
* source string. ChatForm swaps it in once a mention link lands in the
* buffer. Shares the focus()/resetHeight()/caret handle with the textarea.
* The message editor. Renders a plain auto-resizing textarea by default,
* or a ChatFormInputRich that renders `[name](file://...)` mention links as
* inline chips (keeping the value as the markdown source string) once a
* mention link lands in the buffer. The variant is selected via the
* `useRichInput` prop; both share one imperative handle.
*/
export { default as ChatFormContentEditable } from './ChatForm/ChatFormContentEditable.svelte';
/**
* Plain auto-resizing textarea with IME composition support. Default input
* renderer inside ChatForm until a file mention lands.
*/
export { default as ChatFormTextarea } from './ChatForm/ChatFormTextarea.svelte';
export { default as ChatFormInput } from './ChatForm/ChatFormInput/ChatFormInput.svelte';
/**
* Working directory selector for agent mode. Renders a chip below the chat
@@ -288,7 +283,7 @@ export { default as ChatFormTextarea } from './ChatForm/ChatFormTextarea.svelte'
* synthetic "Set working directory to ..." user message into chat history
* and is enforced on tool calls via the `x-tool-cwd` request header.
*/
export { default as ChatFormWorkingDirectory } from './ChatForm/ChatFormWorkingDirectory.svelte';
export { default as ChatFormCurrentWorkingDirectory } from './ChatForm/ChatFormCurrentWorkingDirectory/ChatFormCurrentWorkingDirectory.svelte';
/**
* **ChatFormPickerMcpPrompts** - MCP prompt selection interface
@@ -359,14 +354,14 @@ export { default as ChatFormPickerPopover } from './ChatForm/ChatFormPickers/Cha
* Generic scrollable list for picker popovers. Provides search input,
* scroll-into-view for keyboard navigation, loading skeletons, empty state,
* and optional footer. Uses Svelte 5 snippets for item/skeleton/footer rendering.
* Shared by ChatFormPickerMcpPrompts and ChatFormMentionPicker.
* Shared by ChatFormPickerMcpPrompts and ChatFormPickerMention.
*/
export { default as ChatFormPickerList } from './ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte';
/**
* Generic button wrapper for picker list items. Provides consistent styling,
* hover/selected states, and data-picker-index attribute for scroll-into-view.
* Shared by ChatFormPickerMcpPrompts and ChatFormMentionPicker.
* Shared by ChatFormPickerMcpPrompts and ChatFormPickerMention.
*/
export { default as ChatFormPickerListItem } from './ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte';
@@ -389,14 +384,14 @@ export { default as ChatFormPickerListItemSkeleton } from './ChatForm/ChatFormPi
* tool, scoped to the conversation cwd (or server home when unset).
* Selection splices a `[name](file:///<abs path>)` link into the input.
*/
export { default as ChatFormMentionPicker } from './ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte';
export { default as ChatFormPickerMention } from './ChatForm/ChatFormPickers/ChatFormPickerMention.svelte';
/**
* `/`-triggered slash-command picker. Lists the available slash commands
* (`/prompt`, `/cwd`, `/model`) filtered by the typed query; selection
* hands the command to the parent for dispatch.
*/
export { default as ChatFormCommandPicker } from './ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte';
export { default as ChatFormPickerCommand } from './ChatForm/ChatFormPickers/ChatFormPickerCommand.svelte';
/**
* Hosts the chat-form pickers (slash-command, MCP prompt, file mention)
@@ -25,14 +25,12 @@
DialogMermaidPreview
} from '$lib/components/app';
import {
BOOL_TRUE_STRING,
CODE_BLOCK_CLASS,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED,
DIAGRAM_VIEW_SOURCE,
IMAGE_NOT_ERROR_BOUND_SELECTOR,
MARKDOWN_DATA_ATTRS,
MERMAID_BLOCK_CLASS,
MERMAID_LANGUAGE,
MERMAID_RENDERED_ATTR,
@@ -42,7 +40,7 @@
SVG,
TOGGLE_SOURCE_BTN_CLASS
} from '$lib/constants';
import { ColorMode, UrlProtocol } from '$lib/enums';
import { BooleanString, ColorMode, UrlProtocol } from '$lib/enums';
import { FileTypeText } from '$lib/enums/files.enums';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import { settingsStore } from '$lib/stores';
@@ -486,13 +484,19 @@
const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
if (copyButton && copyButton.dataset.listenerBound !== 'true') {
copyButton.dataset.listenerBound = 'true';
if (
copyButton &&
copyButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
) {
copyButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
copyButton.addEventListener('click', handleCopyClick);
}
if (previewButton && previewButton.dataset.listenerBound !== 'true') {
previewButton.dataset.listenerBound = 'true';
if (
previewButton &&
previewButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
) {
previewButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
previewButton.addEventListener('click', handlePreviewClick);
}
}
@@ -508,7 +512,7 @@
const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
for (const img of images) {
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
img.setAttribute(MARKDOWN_DATA_ATTRS.ERROR_BOUND, BooleanString.TRUE);
img.addEventListener('error', handleImageError);
}
}
@@ -691,7 +695,7 @@
// Mark nodes immediately to prevent duplicate renders if called again during streaming.
// This avoids needing a guard that would block node discovery.
nodes.forEach((node) => node.setAttribute(MERMAID_RENDERED_ATTR, 'true'));
nodes.forEach((node) => node.setAttribute(MERMAID_RENDERED_ATTR, BooleanString.TRUE));
// Read mode before await so Svelte tracks it reactively.
const isDark = mode.current === ColorMode.DARK;
@@ -738,7 +742,7 @@
if (nodes.length === 0) return;
nodes.forEach((node) => {
node.setAttribute(SVG.RENDERED_ATTR, 'true');
node.setAttribute(SVG.RENDERED_ATTR, BooleanString.TRUE);
const source = node.getAttribute(SVG.SOURCE_ATTR) ?? node.textContent ?? '';
const clean = sanitizeSvg(source);
@@ -765,11 +769,11 @@
// Don't handle data URLs or already-handled images
if (
img.src.startsWith(UrlProtocol.DATA) ||
img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING
img.getAttribute(MARKDOWN_DATA_ATTRS.ERROR_HANDLED) === BooleanString.TRUE
)
return;
img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING;
img.setAttribute(MARKDOWN_DATA_ATTRS.ERROR_HANDLED, BooleanString.TRUE);
const src = img.src;
// Create fallback element
@@ -869,13 +873,16 @@
: ''}"
>
{#each renderedBlocks as block (block.id)}
<div class="markdown-block" data-block-id={block.id}>
<div class="markdown-block" {...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: block.id }}>
{@html block.html}
</div>
{/each}
{#if unstableBlockHtml}
<div class="markdown-block markdown-block--unstable" data-block-id="unstable">
<div
class="markdown-block markdown-block--unstable"
{...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: 'unstable' }}
>
<!-- eslint-disable-next-line no-at-html-tags -->
{@html unstableBlockHtml}
</div>
@@ -3,7 +3,14 @@
* Uses dependency injection pattern to avoid direct component state access.
*/
import { MERMAID_BLOCK_CLASS, MERMAID_SYNTAX_ATTR, MERMAID_WRAPPER_CLASS } from '$lib/constants';
import {
CODE_BLOCK_CLASS,
MARKDOWN_DATA_ATTRS,
MERMAID_BLOCK_CLASS,
MERMAID_SYNTAX_ATTR,
MERMAID_WRAPPER_CLASS
} from '$lib/constants';
import { BooleanString } from '$lib/enums';
import { copyCodeToClipboard, copyToClipboard } from '$lib/utils';
export interface PreviewState {
@@ -40,11 +47,11 @@ export function createHandleCopyClick() {
if (!target) return;
const wrapper = target.closest('.code-block-wrapper');
const wrapper = target.closest(`.${CODE_BLOCK_CLASS.WRAPPER}`);
if (!wrapper) return;
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
if (!codeElement) return;
@@ -86,16 +93,16 @@ export function createHandlePreviewClick(previewState: PreviewState) {
if (!target) return;
const wrapper = target.closest('.code-block-wrapper');
const wrapper = target.closest(`.${CODE_BLOCK_CLASS.WRAPPER}`);
if (!wrapper) return;
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
if (!codeElement) return;
const rawCode = codeElement.textContent ?? '';
const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
const languageLabel = wrapper.querySelector<HTMLElement>(`.${CODE_BLOCK_CLASS.LANGUAGE}`);
const language = languageLabel?.textContent?.trim() || 'text';
previewState.setPreviewCode(rawCode);
@@ -112,8 +119,8 @@ export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
return async function handleMermaidClick(event: MouseEvent) {
const target = event.target as HTMLElement;
// Check if clicking on copy or preview button in mermaid block
const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`);
const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`);
const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .${CODE_BLOCK_CLASS.COPY_BTN}`);
const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .${CODE_BLOCK_CLASS.PREVIEW_BTN}`);
if (copyBtn || previewBtn) {
const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`);
@@ -189,15 +196,17 @@ export function createHandleMermaidPreviewOpenChange(mermaidState: MermaidPrevie
export function createHandleImageError(
renderedBlocksState: RenderedBlocksState,
IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
DATA_ERROR_BOUND_ATTR: string,
BOOL_TRUE_STRING: string
errorBoundAttr: string,
booleanString: BooleanString
) {
return async function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
if (!img) return;
const blockId = img.closest('[data-block-id]')?.getAttribute('data-block-id');
const blockId = img
.closest(`[${MARKDOWN_DATA_ATTRS.BLOCK_ID}]`)
?.getAttribute(MARKDOWN_DATA_ATTRS.BLOCK_ID);
if (!blockId) return;
@@ -206,19 +215,22 @@ export function createHandleImageError(
if (!block) return;
// Skip if already handled
if (img.dataset[DATA_ERROR_BOUND_ATTR] === BOOL_TRUE_STRING) return;
if (img.getAttribute(errorBoundAttr) === booleanString) return;
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
img.setAttribute(errorBoundAttr, booleanString);
// Get the fallback HTML and replace the image
const fallbackHtml = `<div class="image-error-placeholder" data-original-src="${img.src}">
const fallbackHtml = `<div class="image-error-placeholder" ${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}="${img.src}">
<span class="image-error-icon"></span>
<span class="image-error-text">Failed to load image</span>
</div>`;
// Replace the img element with fallback in the block's HTML
const newHtml = block.html.replace(/img[^>]*src=["']([^"']*)[^>]*>/g, (match, src) => {
if (src === img.src) {
return fallbackHtml.replace('data-original-src=""', `data-original-src="${src}"`);
return fallbackHtml.replace(
`${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}=""`,
`${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}="${src}"`
);
}
return match;
@@ -243,19 +255,27 @@ export function createSetupCodeBlockActions(
return function setupCodeBlockActions(containerRef: HTMLElement | null) {
if (!containerRef) return;
const wrappers = containerRef.querySelectorAll<HTMLElement>('.code-block-wrapper');
const wrappers = containerRef.querySelectorAll<HTMLElement>(`.${CODE_BLOCK_CLASS.WRAPPER}`);
for (const wrapper of wrappers) {
const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
const copyButton = wrapper.querySelector<HTMLButtonElement>(`.${CODE_BLOCK_CLASS.COPY_BTN}`);
const previewButton = wrapper.querySelector<HTMLButtonElement>(
`.${CODE_BLOCK_CLASS.PREVIEW_BTN}`
);
if (copyButton && copyButton.dataset.listenerBound !== 'true') {
copyButton.dataset.listenerBound = 'true';
if (
copyButton &&
copyButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
) {
copyButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
copyButton.addEventListener('click', handleCopyClick);
}
if (previewButton && previewButton.dataset.listenerBound !== 'true') {
previewButton.dataset.listenerBound = 'true';
if (
previewButton &&
previewButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
) {
previewButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
previewButton.addEventListener('click', handlePreviewClick);
}
}
@@ -269,8 +289,8 @@ export function createSetupCodeBlockActions(
export function createSetupImageErrorHandlers(
handleImageError: (event: Event) => void,
IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
DATA_ERROR_BOUND_ATTR: string,
BOOL_TRUE_STRING: string
errorBoundAttr: string,
booleanString: BooleanString
) {
return function setupImageErrorHandlers(containerRef: HTMLElement | null) {
if (!containerRef) return;
@@ -278,7 +298,7 @@ export function createSetupImageErrorHandlers(
const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
for (const img of images) {
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
img.setAttribute(errorBoundAttr, booleanString);
img.addEventListener('error', handleImageError);
}
};
@@ -2,6 +2,7 @@
* Utility functions for markdown processing in MarkdownContent component.
*/
import { MARKDOWN_DATA_ATTRS } from '$lib/constants';
import type { RootContent as HastRootContent } from 'hast';
/**
@@ -69,7 +70,7 @@ export function getCodeInfoFromTarget(target: HTMLElement): CodeInfo | null {
return null;
}
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
if (!codeElement) {
console.error('No code element found in wrapper');
@@ -17,7 +17,7 @@ import {
createWrapper,
generateBlockId
} from './code-block-utils';
import { CODE_BLOCK_CLASS } from '$lib/constants';
import { CODE_BLOCK_CLASS, MARKDOWN_DATA_ATTRS } from '$lib/constants';
import type { Element, ElementContent, Root } from 'hast';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
@@ -65,16 +65,18 @@ export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
codeElement.properties = {
...codeElement.properties,
'data-code-id': codeId
[MARKDOWN_DATA_ATTRS.CODE_ID]: codeId
};
const actions: Element[] = [createCopyButton(codeId, 'data-code-id', 'Copy code')];
const actions: Element[] = [
createCopyButton(codeId, MARKDOWN_DATA_ATTRS.CODE_ID, 'Copy code')
];
if (language.toLowerCase() === 'html') {
actions.push(createPreviewButton(codeId, 'data-code-id', 'Preview code'));
actions.push(createPreviewButton(codeId, MARKDOWN_DATA_ATTRS.CODE_ID, 'Preview code'));
}
const header = createBlockHeader(language, codeId, 'data-code-id', actions);
const header = createBlockHeader(language, codeId, MARKDOWN_DATA_ATTRS.CODE_ID, actions);
const wrapper = createWrapper(
header,
node,
@@ -1,6 +1,6 @@
/**
* Rehype plugin that rewrites `file://` markdown anchors into the inline
* mention chip, sharing the class string with the contenteditable
* mention chip, sharing the class string with the ChatFormInputRich
* tokenizer via `$lib/constants`.
*
* The chip is presentational: `file://` navigation is blocked from
@@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from '$app/environment';
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX, UI_DATA_ATTRS } from '$lib/constants';
import { BooleanString, ColorMode } from '$lib/enums';
import { highlightCode } from '$lib/utils';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
@@ -38,13 +38,15 @@
function loadHighlightTheme(isDark: boolean) {
if (!browser) return;
const existingThemes = document.querySelectorAll('style[data-highlight-theme-preview]');
const existingThemes = document.querySelectorAll(
`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`
);
existingThemes.forEach((style) => style.remove());
const style = document.createElement('style');
style.setAttribute('data-highlight-theme-preview', 'true');
style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
style.textContent = isDark ? githubDarkCss : githubLightCss;
document.head.appendChild(style);
@@ -4,14 +4,12 @@
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import {
BOOL_FALSE_STRING,
BOOL_TRUE_STRING,
DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY,
HEADERS,
MCP_SERVER_ID_PREFIX,
RECOMMENDED_MCP_SERVERS
} from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { BooleanString, HealthCheckStatus } from '$lib/enums';
import { conversationsStore, mcpStore } from '$lib/stores';
import { canonicalizeServerUrl, parseHeadersToArray, uuid } from '$lib/utils';
@@ -97,9 +95,9 @@
if (!raw) return false;
if (raw === BOOL_TRUE_STRING) return true;
if (raw === BooleanString.TRUE) return true;
if (raw === BOOL_FALSE_STRING) return false;
if (raw === BooleanString.FALSE) return false;
try {
const parsed = JSON.parse(raw);
@@ -116,7 +114,7 @@
if (browser) {
localStorage.setItem(
DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY,
dismissed ? BOOL_TRUE_STRING : BOOL_FALSE_STRING
dismissed ? BooleanString.TRUE : BooleanString.FALSE
);
}
}
@@ -3,6 +3,7 @@
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { UI_DATA_ATTRS } from '$lib/constants';
import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
import { SvelteSet } from 'svelte/reactivity';
@@ -138,7 +139,7 @@
class="cursor-pointer border-b transition-colors hover:bg-muted/50 {checked
? 'bg-muted/75'
: ''}"
data-conversation-row={conv.id}
{...{ [UI_DATA_ATTRS.CONVERSATION_ROW]: conv.id }}
onmousedown={(event) => marquee.rowMouseDown(conv.id, event)}
onclick={(event) => marquee.rowClick(conv.id, event.shiftKey)}
>
@@ -15,7 +15,7 @@
import { TruncatedText } from '$lib/components/app';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Tooltip from '$lib/components/ui/tooltip';
import { FORK_TREE_DEPTH_PADDING, ICON_CLASS_DEFAULT } from '$lib/constants';
import { FORK_TREE_DEPTH_PADDING, ICON_CLASS_DEFAULT, UI_DATA_ATTRS } from '$lib/constants';
import { RouterService } from '$lib/services/router.service';
import { chatStore, conversationsStore } from '$lib/stores';
import { onMount } from 'svelte';
@@ -154,14 +154,13 @@
});
</script>
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
<button
class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
? 'bg-foreground/5 text-accent-foreground'
: ''} {isSelected ? 'bg-primary/10 hover:bg-primary/15' : ''} {isSelectionMode
? 'is-selection-mode'
: ''} px-2"
data-conversation-row={conversation.id}
{...{ [UI_DATA_ATTRS.CONVERSATION_ROW]: conversation.id }}
onclick={(e) => handleSelect(e)}
onmouseover={handleMouseOver}
onmouseleave={handleMouseLeave}
@@ -1,6 +1,7 @@
<script lang="ts">
import { ChevronLeft, ChevronRight, Settings } from '@lucide/svelte';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ICON_CLASS_DEFAULT, UI_DATA_ATTRS } from '$lib/constants';
import { BooleanString } from '$lib/enums';
import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte';
import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
import { onMount, tick } from 'svelte';
@@ -20,7 +21,9 @@
await tick();
if (carousel.scrollContainer) {
const activeTab = carousel.scrollContainer.querySelector('[data-active="true"]');
const activeTab = carousel.scrollContainer.querySelector(
`[${UI_DATA_ATTRS.ACTIVE}="${BooleanString.TRUE}"]`
);
if (activeTab instanceof HTMLElement) {
carousel.scrollToCenter(activeTab);
@@ -66,7 +69,7 @@
)
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
data-active={isActive(section)}
{...{ [UI_DATA_ATTRS.ACTIVE]: isActive(section) }}
href={getHref(section)}
onclick={(e: MouseEvent) => {
carousel.scrollToCenter(e.currentTarget as HTMLElement);
@@ -82,7 +85,7 @@
)
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground'}"
data-active={isActive(section)}
{...{ [UI_DATA_ATTRS.ACTIVE]: isActive(section) }}
onclick={(e: MouseEvent) => {
onSectionChange?.(section.title);
carousel.scrollToCenter(e.currentTarget as HTMLElement);
@@ -1,3 +1,6 @@
/** Data attribute that tags ChatFormInputRich code spans and blocks. */
export const CODE_TOKEN_ATTR = 'data-code-token';
export const INITIAL_FILE_SIZE = 0;
export const PROMPT_CONTENT_SEPARATOR = '\n\n';
export const CLIPBOARD_CONTENT_QUOTE_PREFIX = '"';
@@ -1,3 +1,6 @@
/** Number of trailing characters to keep visible when partially redacting mcp-session-id */
const MCP_SESSION_ID_VISIBLE_CHARS = 5;
/** HTTP header handling for API and MCP requests. */
export const HEADERS = {
/** Canonical casing for the Authorization header (RFC 7235) */
@@ -7,7 +10,7 @@ export const HEADERS = {
/** Content-Type HTTP header name */
CONTENT_TYPE: 'Content-Type',
/** Partial-redaction rules for MCP headers: header name -> visible trailing chars */
PARTIAL_REDACT: new Map<string, number>([['mcp-session-id', 5]]),
PARTIAL_REDACT: new Map<string, number>([['mcp-session-id', MCP_SESSION_ID_VISIBLE_CHARS]]),
/** Header names whose values should be redacted in diagnostic logs */
REDACTED: new Set([
@@ -1,8 +1,14 @@
export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])';
export const DATA_ERROR_BOUND_ATTR = 'errorBound';
export const DATA_ERROR_HANDLED_ATTR = 'errorHandled';
export const BOOL_TRUE_STRING = 'true';
export const BOOL_FALSE_STRING = 'false';
/** Data attributes for the markdown renderer DOM contract. */
export const MARKDOWN_DATA_ATTRS = {
BLOCK_ID: 'data-block-id',
CODE_ID: 'data-code-id',
ERROR_BOUND: 'data-error-bound',
ERROR_HANDLED: 'data-error-handled',
LISTENER_BOUND: 'data-listener-bound',
ORIGINAL_SRC: 'data-original-src'
} as const;
/** Markdown structural markers used by `looksLikeMarkdown`. Inline / line-level. */
export const MARKDOWN = {
@@ -1,6 +1,6 @@
/**
* Shared visual contract between the two DOM-only badge paths (the
* contenteditable tokenizer + the rehype plugin). Svelte cannot be
* ChatFormInputRich tokenizer + the rehype plugin). Svelte cannot be
* mounted at the per-keystroke tokenizer hot path nor from a hast tree,
* so both emit the badge with the same class string literal; Tailwind's
* scanner picks it up in both sources.
@@ -10,6 +10,13 @@ export const MENTION_BADGE_CLASSNAME =
export const MENTION_BADGE_ICON_CLASSNAME = 'h-3 w-3 shrink-0';
/** Full `data-*` attribute names that tag ChatFormInputRich mention badges. */
export const MENTION_BADGE_DATA_ATTRS = {
BADGE: 'data-mention-badge',
NAME: 'data-mention-name',
PATH: 'data-mention-path'
} as const;
/** Regex flag that makes the mention scanner walk every link in a message instead of the first. */
export const MENTION_LINK_SCAN_FLAGS = 'g';
@@ -7,6 +7,16 @@ import type { DesktopIconStripItem } from '$lib/types';
export const FORK_TREE_DEPTH_PADDING = 8;
export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
/** Data attributes for app-level DOM contracts. */
export const UI_DATA_ATTRS = {
ACTIVE: 'data-active',
CONVERSATION_ROW: 'data-conversation-row',
HIGHLIGHT_THEME_PREVIEW: 'data-highlight-theme-preview',
PICKER_INDEX: 'data-picker-index',
RESULT_INDEX: 'data-result-index',
THUMBNAIL_INDEX: 'data-thumbnail-index'
} as const;
export const TOOL_GROUP_LABELS = {
[ToolSource.BUILTIN]: 'Built-in',
[ToolSource.CUSTOM]: 'JSON Schema',

Some files were not shown because too many files have changed in this diff Show More