Compare commits

...
25 Commits
Author SHA1 Message Date
511f9c1379 OpenVINO: Update OV to 2026.3.1, whisper.cpp support, Qwen3.5 on NPU, and new ops (#27843)
* OpenVINO Backend: Fuse IM2COL + MatMul convolution into OpenVINO convolution

* ci:ggml-ov: Skip recurrent state rollback tests

* ci:ggml-ov: Skip recurrent state rollback tests

* Update OPENVINO.md

* ggml-openvino : add env-var gated op support debugging

* Fix ggml_rope_set_offset case

* OpenVINO backend: Support Whisper.cpp

* Fix code style

* openvino : enable qwen35 on NPU

Static shapes:
- get_graph_input_shape() left the s_copy / s_copy-leaf inputs dynamic
  ([1,1,1,-1]) even in static mode, which propagated a dynamic slot dim through
  GET_ROWS into the conv/GDN state, the state reshapes and the GDN output.
- With -np 1 the s_copy defrag remainder gathers zero rows; short-circuit that
  CPY to the untouched cache instead of emitting a degenerate Slice/Concat, and
  skip binding its zero-byte ggml tensor as an output (the dynamic path already
  did the latter, the static path wrote the full cache over a 0-byte buffer).

Token-count independence:
- In static mode the compiled model's token count is the prefill chunk size or
  1, not the captured cgraph's. Offsets derived from the captured count were
  therefore wrong. Anchor the GDN state slice at the end of the packed
  [attn | state] output and drop the rs_src_begin runtime inputs, and make
  VIEWs over the GDN output / conv_input pass through so the consumer does the
  slicing.
- CONT could not identify its token axis when the graph was captured with a
  single token (every trailing dim has the same stride and size 1) and baked
  the captured shape into the prefill model.

Chunked prefill:
- The last chunk is padded with fabricated tokens. Attention masks them, but
  the recurrent path folded them into cache_r/cache_s permanently. Add a
  chunk_valid_len runtime input, use it to zero g and beta for padded steps
  (making the recurrence an exact identity) and to end the conv snapshot window
  at the last valid token, and disable the recurrent-cache reset after the
  first chunk so earlier chunks are not wiped.
- get_is_prefill() and the chunk loop bound read inp_pos->ne[0] directly, but
  IMROPE stacks 4 position planes, so every decode step was run through the
  padded prefill model and the loop ran extra out-of-bounds chunks.

cache_rs_reset_idx/len now stay runtime Parameters in static mode, since
can_reuse_statically() does not invalidate the cached model on ComputeParams
changes. Add GGML_OPENVINO_FORCE_STATIC to exercise the static path on CPU.

* Update to OpenVINO 2026.3.1

* ggml-openvino: forward NPU compilation mode parameters

Add GGML_OPENVINO_NPU_COMPILE_CONFIG to the backend's cached environment so callers can configure the NPU compiler without using the generic property escape hatch.

When the value is non-empty, pass it to OpenVINO as NPU_COMPILATION_MODE_PARAMS. This enables settings such as optimization-level=3 for NPU compilation while preserving the existing behavior when the variable is unset and leaving CPU and GPU configuration unchanged.

Document the variable, its NPU-only scope, and the optimization-level=3 example in the OpenVINO backend runtime configuration table.

* ggml-openvino : support RELU, POOL_2D, QUICK_GEGLU, and ROLL ops

* reorder op table

* exclude GPU/NPU failing POOL_2D case

* move op type detection to compute_op_case

* Relax rope supported cases

* Fix pool case

* Update openvino doc, gpu driver in ov docker

* openvino: remove unused static remote context branch

* openvino: parallelize static model build

* Apply editorconfig

---------

Co-authored-by: Mostafa Faheem <mostafaaafaheem@gmail.com>
Co-authored-by: Ravi Panchumarthy <ravi.panchumarthy@intel.com>
Co-authored-by: zhaixuejun1993 <xuejun.zhai@intel.com>
2026-08-28 14:42:07 +03:00
Xuan-Son NguyenandGitHub b19cbe925b convert: prevent ndarray conversion in LazyChunkedTensor (#27869) 2026-08-28 11:46:30 +02:00
Ozymandias_EBONandGitHub d077b4c214 sycl: use TILE for quantized KV decode on BMG (#26689)
Route quantized KV decode to TILE on Xe2 (BMG) only, keep VEC on other archs until validated there.
2026-08-28 11:58:58 +03:00
TitaniumtownandGitHub be876204aa sycl: bind the f16 KV cache in place for the oneDNN SDPA path (#27468)
Measured at a live KV length of 34816 (32768 depth plus one 2048 ubatch),
on Qwen3.8 27B Q4_K_S:

  per tensor         4 * 34816 * 256 * 2 B  =  71.3 MB
  staged per call    K and V, so 2x         = 142.6 MB
  traffic per call   read once, write once  = 285.2 MB
  traffic per ubatch 285.2 MB * 16 calls    =   4.56 GB

One ubatch is one ggml_cgraph submission (llama_context::process_ubatch ->
graph_compute), so that 4.56 GB is the cost of a single 2048-token prefill
chunk, and it scales with the live KV length: the first ubatch of the same run,
at seq = 2048, moves 0.27 GB.

Reproduce the two measured inputs with:

  GGML_SCHED_DEBUG=2 llama-bench -m MODEL -p 8 -n 0 -r 1 -ngl 0 \
      -fa on -ctk f16 -ctv f16 -v > nd.txt 2>&1
  grep -E 'n_layer|n_head_kv|n_embd_head_k' nd.txt
  awk '/node #  0 /{g++} g==1 && /\(FLASH_ATTN\)/{n++} END{print n+0}' nd.txt
2026-08-28 11:53:31 +03:00
Georgi GerganovandGitHub 8963a9bdcd metal : add fa-vec tunings for M3 Max, M5 and M5 Pro (#27863)
* metal : add fa-vec tunings for M5

This is a followup contribution to efeda76b94 as requested in https://github.com/ggml-org/llama.cpp/discussions/27668 to add support for additional Apple GPUs. I generated this output using the provided instructions:

```sh
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

cmake -B build -DGGML_METAL=ON
cmake --build build --target ggml-metal-tuning -j

./build/bin/ggml-metal-tuning fa-vec --dtype f16,q8_0 > fa_vec_rows.txt 2> fa_vec_sweep.log
```

This ran on a machine with Apple M5.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* metal : add fa-vec tunings for M5 Pro

This adds fa_vec_tuned_table records for Apple M5 Pro to ggml-metal-tuning.cpp.

Contributed by SerayaEryn in https://github.com/ggml-org/llama.cpp/discussions/27668#discussioncomment-18157544 (F16, Q4_0, Q8_0; M5 Pro, 20 GPU cores).

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* metal : add fa-vec tunings for M3 Max

This adds fa_vec_tuned_table records for Apple M3 Max to ggml-metal-tuning.cpp.

Contributed by TeeAaTeeUu in https://github.com/ggml-org/llama.cpp/discussions/27668#discussioncomment-18175220 (F16, Q8_0; M3 Max, MacBook Pro 64GB, low power mode).

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* cont : whitespaces
2026-08-28 11:52:03 +03:00
Brad SmithandGitHub 6d6b697cd5 metal : add fa-vec tunings for M4 Pro (#27824)
This is a followup contribution to efeda76b94 as requested in https://github.com/ggml-org/llama.cpp/discussions/27668 to add support for additional Apple GPUs. I generated this output using the provided instructions:

```sh
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

cmake -B build -DGGML_METAL=ON
cmake --build build --target ggml-metal-tuning -j

./build/bin/ggml-metal-tuning fa-vec --dtype f16,q8_0 > fa_vec_rows.txt 2> fa_vec_sweep.log
```

This ran on a MacBook Pro (14-inch, Nov 2024) with Apple M4 Pro. The `ggml-metal-tuning` command completed successfully in 1h 13m 1s with no other notable load on the system.
2026-08-28 11:37:43 +03:00
Georgi GerganovandGitHub 4e97ac86eb tests : run test-save-load-state across all architectures (#27755)
* tests : run test-save-load-state across all architectures

test-save-load-state previously only ran in ctest against a single
downloaded model (tinyllamas/stories15M), i.e. only the llama arch.

Add a --models DIR mode to test-save-load-state that runs the full
save/load suite over every *.gguf in a directory, reporting a
per-model PASS/FAIL and exiting non-zero if any model fails, and wire
a ctest to run it over all architectures using the existing
generate-models fixture (test-llama-archs). The single-model -m mode
is preserved (still used by ci/run.sh).

Also bump the dummy-model training context in test-llama-archs from
128 to 256 so that the per-sequence context (which is padded up to a
multiple of 256) no longer exceeds n_ctx_train and emits the
"possible training context overflow" warning.

The test is expected to fail until the affected arches are fixed:
deepseek4 (host seq-copy), gemma2/gpt-oss/lfm2 (device seq-copy),
minimax-01 (state load). It aborts at the first arch that crashes.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* tests : match dummy DSA indexer to fused Lightning Indexer kernel

The dummy DSA indexer (deepseek32, glm-dsa, ...) used key_length=64 and head_count=1, so the fused Lightning Indexer op's q tensor was shaped [64, 1, ...]. The Metal fused kernel is fixed to DK=128, NH=64, so it rejected the op and the scheduler fell back to CPU, emitting a 'layer assigned to MTL but Lightning Indexer on CPU' warning. Bump key_length to 128 and the DSA head_count to 64 so the fused op runs on the GPU.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* tests : add --help and document -o in test-llama-archs

Add a --help/-h flag to test-llama-archs and list the existing -o/--out option in the usage text, which was previously missing.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* tests : use 64 indexer heads for deepseek4

deepseek4's indexer head count was set to n_head (8), which does not match the fused Lightning Indexer kernel's fixed NH=64, so the fused op fell back to the CPU backend and emitted a device-mismatch warning. Give it the same fixed 64 as the other indexer archs by dropping it from the n_head ternary (only minimax-m3 keeps n_head, since it does not use the fused Lightning Indexer op).

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* tests : fix dsv4 save-load n_stream mismatch

The dsv4 KV cache keeps per-sequence KV/state streams even in unified mode, so its n_stream equals n_seq_max. The test saved the state in the baseline with n_seq_max=1 but loaded it in the seq-copy tests with n_seq_max=2, so state_read threw an n_stream mismatch. Use n_seq_max=2 in the baseline and state-load tests so the save and load agree.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* context : relax on-device seq-copy chunk alignment

The on-device state seq copy (llama_state_seq_set_data with LLAMA_STATE_SEQ_FLAGS_ON_DEVICE) copied the write-side cpy tensors to the read-side targets 1:1 by index, requiring the writer and reader to emit the same number of chunks in the same order with the same per-chunk sizes. state_write_data chunks per cell-range while state_read_data chunks contiguous-or-per-cell, so the counts diverged for non-contiguous sources (dsv4, SWA) and the copy aborted with "memory buffer mismatch".

All state writers and readers enumerate the same logical data in the same order, differing only in chunking. Copy the flat write-side data into the read-side targets with a byte cursor that walks both tensor lists across their boundaries, so the chunking no longer needs to match. Keep the total-size guard; drop the n_tensors equality check.

Assisted-by: pi:llama.cpp/Qwen3.8-27B

* model : fix dangling hparams ref in minimax-01 LA graph input
llm_graph_input_la stored const llama_hparams & hparams, bound to the llm_graph_params temporary in llama_context::process_ubatch. The input object outlives that temporary (it is kept in llm_graph_result::inputs for graph reuse), so set_input() read destroyed stack memory on every graph reuse - test-save-load-state crashed for minimax-01 when the stack region was overwritten (n_layer_all read as 0, abort in llama_hparams::n_head). Store a copy like every other graph input class.
Assisted-by: pi:llama.cpp/Qwen3.8-27B

* context : handle "worst case" graph and add TODO
2026-08-28 09:45:19 +03:00
ca3d5a3e10 model: add DSpark support for Nemotron3.5 (#27804)
* model: add DSpark support for Nemotron3.5

* Update src/models/dflash.cpp

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>

---------

Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-08-28 01:49:27 +02:00
cqderekandGitHub e70802a01f ggml-hexagon: add HTP unary ops for ABS and LOG (#27786)
Add HVX-accelerated implementations for GGML_OP_LOG and
GGML_UNARY_OP_ABS on the HTP backend.

- Register HTP_OP_UNARY_ABS and HTP_OP_UNARY_LOG in op_remap_to_htp()
- Add ABS and LOG to ggml_backend_hexagon_device_supports_op()
- Implement hvx_abs_f32_aa() in hvx-arith.h using hvx_vec_abs_f32()
- Implement hvx_log_f32_aa() in hvx-log.h using hvx_vec_log_f32()
- Add abs_f32() and log_f32() row-wise dispatch in unary-ops.c
- Define tiled and non-tiled task functions via DEFINE_UNARY_TASK and
  DEFINE_UNARY_TILED_TASK macros
- Route HTP_OP_UNARY_ABS and HTP_OP_UNARY_LOG through execute_op()
  in main.c
2026-08-27 15:05:57 -07:00
Aparna M PandGitHub 83d855c5a6 hex-unary: fix RMS_NORM_MUL weight-offset bugs for grouped/broadcast norms (#27798) 2026-08-27 14:38:02 -07:00
18443257a3 server: add ctx-per-slot (--kv-unified-per-slot) (#24124)
* Add ctx-per-slot argument for unifid KV cache

* Swap out ctx fractions for ctx pool slots

* Formatting cleanup

* Remove ctx-pool-slots, make ctx-per-slot an int

* refactor it

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-08-27 22:39:14 +02:00
Harkirat GillandGitHub 32176338a6 ci : build only the ggml-hip backend for windows-rocm release (#27753) 2026-08-27 22:18:34 +02:00
6c84c7d5d8 model: add Qwen3.8-Flash-Next (qwen4exp) (#27742)
* gguf: add qwen4exp (Qwen3.8-Flash-Next) arch and converter

Adds the GGUF-side plumbing for HF model_type qwen4_exp:

- MODEL_ARCH.QWEN4EXP plus tensors for the low-rank hyper-connection
  variant (hc_*_norm/down/up/inject) and the PLE n-gram hash embeddings.
  The DeepSeek-V4 hc_*_fn/base/scale tensors are a different
  parameterisation, so these are separate entries rather than reuse.
- Reuses the existing indexer, per_layer_token_embd, SSM and
  compress_ratios keys unchanged.
- conversion/qwen4exp.py inherits the Qwen3.5 linear-attention V-head
  reorder and interleaved mrope, concatenates the 128 PLE embedding
  shards, and splits index_qk_proj into separate indexer q/k tensors.

The PLE hash multipliers reach ~2.4e13. prepare_tensors() casts every
non-float dtype to float32 before modify_tensors() runs, and GGUF array
writes infer INT32 from Python ints, so both paths are bypassed: the
constants are read from the pre-cast lazy tensors and written as
explicit UINT64 arrays.

Additive only; no existing arch changes behaviour.

* llama: load qwen4exp (Qwen3.8-Flash-Next) hparams and tensors

Adds LLM_ARCH_QWEN4EXP with its hparams and tensor loading. The graph
comes in the next commit; this makes the model load and report correct
metadata.

- hyper-connections set n_embd_out_impl = hc_count * n_embd, so the
  residual stream is 4x wide and there is no output_norm: the final
  mixer's hc_norm is the last norm in the model.
- registered as hybrid and given the same recurrent/attention memory
  filters as Qwen3-Next and Qwen3.5.
- reuses the existing indexer, per_layer_token_embd, SSM and
  compress_ratios keys as-is.
- the PLE table row count is read back from the file rather than
  recomputing the vocab padding rule.

llama-model-loader gains UINT64 array support. That branch previously
threw, so no existing caller changes behaviour; it is needed because the
PLE hash multipliers do not fit in int32.

* qwen4exp: shorten comments

* llama: qwen4exp text graph with hyper-connections, GDN and MoE

Implements the decode graph for Qwen3.8-Flash-Next: the hyper-connection
residual stream, gated delta net layers, the MoE block with its gated shared
expert, and dense full attention. The QSA indexer and the PLE n-gram embedding
are not wired up yet and land in later commits.

Hyper-connections are implemented here rather than shared with deepseek4.cpp.
The two formulations agree on the [n_embd, hc, n_tokens] layout and little
else: DeepSeek-V4 mixes with a full-rank projection and Sinkhorn-normalises
it, whereas this model uses a low-rank down/silu/up sigmoid gate and collapses
by a plain mean. Only the ~10 line stream mean is genuinely common, so sharing
would mean touching DSV4's hot path and its three fused CUDA ops to reuse very
little. What is reused is the substantive part: the LLM_KV_HYPER_CONNECTION_*
keys, the n_embd_out_impl wide-residual support already in the loader, and the
layout convention.

Also allows a checkpoint to carry no PLE layers at all, which makes it
possible to bring the graph up and validate it in stages.

Validated against vLLM, the only working reference implementation. On a
scaled-down model with an init scale large enough to give non-uniform logits,
agreement with vLLM sits at the numerical noise floor: llama.cpp f32 against
its own bf16 gives 84.3% top-1 agreement over 255 positions, and this graph
against vLLM gives 85.1%. The comparison was calibrated by seeding three
deliberate bugs (silu instead of sigmoid on the delta net gate, dropping the
1/hc scale in the mix, dropping the 2x in the combine); each drops top-1 to
between 0% and 11%, an order of magnitude below the floor.

* llama: qwen4exp PLE n-gram hash embedding

Adds the per-layer embedding: a custom I32 graph input hashes each token with
its ngram_size-1 predecessors host-side and the result is a plain row gather
over the shared table, the same shape gemma3n's per-layer embedding uses. The
hash has to run on the host because the splitmix64-derived multipliers reach
2^45, so the products need 64-bit integers and an xor, neither of which ggml
has.

Predecessors that fall outside the ubatch come from a small per-sequence
history on the model, mirroring the per-request ngram_context the reference
carries. It is only trusted when contiguous with the incoming position, so a
fresh prompt or a rewound cache falls back to EOS padding rather than hashing
against stale tokens.

The depthwise conv is written out as a sum of shifted, per-channel-scaled
copies rather than through ggml_conv_1d_dw, which carries a correctness
warning upstream.

Verified two ways. The row indices match a transcription of the reference's
tensor formulation exactly, 1024 of 1024 rows, including sequences with EOS
tokens sprinkled through them to exercise the segment reset. Separately, with
PLE placed on layer 0 so its input is just the token embedding, ple_embd and
ple_gated_value match a PyTorch computation from the same checkpoint to every
printed digit.

End to end over 1023 scored positions the port sits the same distance from
vLLM with PLE as without it, 6.3 points of top-1 against 6.0, so PLE costs no
accuracy relative to the rest of the model. That common offset is vLLM's bf16
activations, which cannot be removed: its QSA kernel refuses float32.

Two bugs found along the way, both caught by the row-index check. The history
was read and updated in the same pass, so a token early in a ubatch could pick
up an earlier token of that same ubatch as prior context; it is now snapshotted
first. And an EOS token was cutting its own context, where the reference takes
the last EOS strictly before the position, so a boundary only hides tokens from
the positions after it.

Known gap: the conv carries no state across ubatches, so it is exact only for a
prefill that starts at position 0. Chunked prefill and decode need the conv
state wired into the recurrent memory, and the conv branch itself is still
numerically unverified because the fixture zeroes its weights.

* llama: carry the qwen4exp PLE conv state across ubatches

The PLE depthwise conv was zero-padding on the left, which is only right for a
prefill that starts at position 0. Decode and chunked prefill saw a truncated
history for the first (kernel-1)*ngram_size positions of every ubatch.

The PLE module sits on a layer that is also a delta-net layer, so both need a
conv history in the same recurrent row. Rather than plumb a per-layer state
size through build_rs and build_conv_state, the row is widened once and each
convolution addresses its own slice through a local helper. n_embd_r() gains
the extra span, which is zero for every other architecture because it is
derived from ple_n_heads.

Verified by feeding the same 1024 token sequence in chunks instead of one
shot: at 64 tokens per decode the logits are bit-identical to the single-shot
run, 1023 of 1023 top-1 and a maximum logprob deviation of exactly zero. At
one token per decode they differ slightly, but the no-PLE model differs more
under the same test (94.6% against 97.1%), so that is the usual gemv-versus-
gemm accumulation difference and not the state.

The conv branch is also no longer unverified. With non-zero conv weights the
port sits 6.3 points of top-1 below the numerical floor, the same distance as
with the weights zeroed and as the model with no PLE at all, so the branch
adds no error of its own.

test-llama-archs passes every existing architecture at 0.00e+00, including the
delta-net models that share this code path.

* llama: fix the qwen4exp PLE conv state and unblock test-llama-archs

build_rs writes into the state tensor in place, zeroing one row and copying the
carried-over states, so calling it twice for the same layer let the second call
clobber the first write-back. The PLE layer is also a delta-net layer, so that
is exactly what happened: both convolutions gathered the same row. They now
share a single gather per layer.

The earlier claim that the conv state was carried correctly was tested on a
fixture whose conv weights are zero, where the branch contributes nothing and
chunking matches trivially. Re-running with non-zero conv weights showed the
divergence, growing with the number of ubatch boundaries: 97.1% top-1 at one
boundary down to 90.2% at seven. With the shared gather it is bit-identical to
the single-shot run at every chunk size tried, 512, 128 and 64, with a maximum
logprob deviation of exactly zero over 1023 positions. The delta-net-only model
stays bit-identical too, so nothing regressed there.

Also derive the delta-net conv channel count the way load_arch_tensors sizes
wqkv instead of from ssm_d_inner. The two agree for this model, but n_embd_r()
only bounds the row and the convolution has to match the tensor feeding it.

test-llama-archs previously aborted on this architecture and took every later
architecture with it. qwen4exp is marked MoE-only, given the hyper-connection
keys and an ssm_d_inner consistent with its tensor derivation, and skipped for
now: the hyper-connection keys written by get_gguf_ctx are not reaching the
synthesised file, which needs a separate look. The suite completes again, 124
architectures at 0.00e+00.

* llama: optional indexer key cache in llama_memory_hybrid

Groundwork for qwen4exp's QSA sparse attention. Its indexer needs a per-token
key history for the full-attention layers, but a hybrid model cannot use
llama_kv_cache_dsa: that class derives from llama_memory_i rather than
llama_kv_cache, and llama_memory_hybrid constructs its attention cache
directly. No existing architecture pairs recurrent state with a sparse
indexer, so there was nothing to reuse wholesale.

llama_memory_hybrid therefore gains a third, optional cache, shaped the same
way llama_kv_cache_dsa shapes its lightning-indexer cache: a copy of hparams
with n_head_kv forced to 1 and n_embd_head_k_full set to indexer_head_size.
It is built only when a filter_idx callback is passed, which defaults to
nullptr, so every existing architecture gets exactly what it got before. The
per-sequence operations and the batch preparation forward to it under a null
check, matching how the DSA cache prepares its two caches over the same
ubatches.

test-llama-archs passes all 124 architectures at 0.00e+00, including the 12 in
the hybrid family that share this code. The qwen4exp fixtures are unchanged:
same logits against vLLM, and chunked evaluation still bit-identical to
single-shot.

* llama: QSA sparse attention for qwen4exp

The full-attention layers of this model do not attend to everything. An
indexer scores one mean-pooled key per block of compress_ratio tokens and
keeps a budget of the best blocks, plus the tail of tokens that do not yet
form a complete block. Below indexer_top_k + compress_ratio - 1 cached
tokens every block fits in the budget, so the result is exactly dense.

What is reused rather than rebuilt:

  - the mask machinery. build_attn's DSA overload already turns a list of
    token indices into a KQ mask via ggml_set_rows, so that block is lifted
    out verbatim into build_attn_mask_top_k and shared with a new overload
    on llm_graph_input_attn_kv. DSA's node sequence is unchanged; the new
    overload exists because llama_kv_cache_dsa assumes MLA and cannot be
    dropped into a hybrid model.
  - the indexer key cache, which is the optional third cache added to
    llama_memory_hybrid in the previous commit. It holds raw keys, because
    pooling happens before the norm and the rotation.

The graph expands block scores rather than block indices: giving every
token of a block its block's score needs only a gather, where expanding
indices would need an integer multiply-add that ggml has no op for. Since
the budget is a whole number of blocks and a block's members tie exactly,
the cut still lands on a block boundary.

Everything that depends on cache layout is computed host-side in
set_input_qsa. Blocks are cuts of the position line rather than of the cell
array, so nothing assumes the cache is contiguous.

Measured on the tiny fixture against vLLM, comparing the selected token
indices directly rather than the logits:

  below the budget    selection identical, and 1024-token logits are
                      bit-identical to the pre-QSA dense path
  above the budget    mean jaccard 0.975

The direct index comparison is what made this correct. The reference
rectifies each head's dot product before summing over heads, which an
earlier reading of it had missed; on logits alone the resulting port looked
fine, because on a randomly initialised fixture the known-correct dense
path already disagrees with vLLM by more than the bug did. Comparing the
indices showed 0.794, and fixing the ReLU moved it to 0.975.

* llama: give the qwen4exp indexer cache the attention cache's slots

The indexer cache found its own slots, independently of the attention
cache. Both are the same size and see the same ubatches, so in a
straight-through prefill they agree, which is why every fixture and every
single-shot parity run passed. They drift once the context is being
rewritten between turns, and then the QSA top-k indices, which are applied
against the attention mask, point at the wrong cells.

The seven-turn chat test caught it on the third turn: llama-server aborted
on the assertion that the two caches report the same n_kv.

The cache is a side buffer addressed by the attention cache's cells, so it
now takes that cache's slot layout instead of computing one. Applying that
layout also marks its cells identically, so the two agree cell for cell by
construction rather than by coincidence, and the assertion can no longer
fire.

Inert where the caches already agreed: test-llama-archs green at 126 archs
and 0.00e+00, and the 4096-token tiny fixture is unchanged at max logit
delta 0.0.

* tests: record what the qwen4exp arch-test skip actually observes

The old note guessed that the hyper-connection keys never reach the file.
They do: dumping the gguf_context handed to llama_model_init_from_user
shows both among its 67 KVs, and the loader still reports one missing.

* tests: cover qwen4exp in test-llama-archs

The arch was skipped with a note guessing that the hyper-connection keys
never reached the synthesised file. They did. The suite builds a model, then
saves and reloads it, and llama_model_saver did not re-emit those keys, so
the failure was in the roundtrip leg rather than the first load. Three gaps,
all in shared code and all additive:

  - add_kv_from_model wrote no hyper-connection, compress-ratio or PLE keys.
    The PLE group only means anything whole, so it is written or omitted
    together; the rest follow the file's existing style of writing every key
    unconditionally, since an architecture that does not read one is
    unaffected by a zero.
  - the saver had no uint64 path at all, which the PLE hash constants need.
  - add_tensors_from_model enumerates model-level tensors by hand and was
    missing per_layer_tok_embd and the three final-mixer tensors.

Two smaller fixes on the qwen4exp side, both found by running the test:

  - build_qsa_top_k divided by the compression ratio before asserting it was
    non-zero, so a file without the key crashed instead of reporting.
  - a layer with no compression ratio now falls back to dense attention,
    which is what the model computes below the budget anyway. The test then
    has to write a ratio to reach QSA at all, and an indexer key length no
    narrower than n_rot, since the indexer ropes with the main attention's
    rotary width.

Full suite: 126 archs, qwen4exp at 0.00e+00 with roundtrip OK. The tiny
fixture is unchanged, max logit delta 0.0 against the pre-QSA dense run.

* convert: stream the qwen4exp PLE table instead of concatenating it

The n-gram table arrives as 128 shards that were held in a dict and then
torch.cat-ed, so the peak was the shards plus the concatenation: around
300 GB of RSS on the real checkpoint, which rules out machines that could
otherwise convert this model.

Each shard is now written straight into a memory-mapped file at its final
row offset and dropped, so the resident set is one shard and the rest is
the page cache's problem. The temporary file sits beside the output and is
removed once the write finishes, including on failure.

Shards other than the last must be uniform for direct placement, which is
asserted rather than assumed, and a shard arriving before the stride is
known is held instead of misplaced.

Verified on the tiny fixture: the resulting GGUF is byte-identical to the
one the concatenating path produced (md5 2d274efac91ad1e9a6007efb0687e597).

* quantize: fall back to F16 for 32-block types with an odd ncols

tensor_type_fallback demotes a tensor whose ncols is not a multiple of the
target's block size, but its switch only enumerates the 256-block types. A
target that is already a 32-block type (iq4_nl, q4_0, q5_0, q8_0, ...) falls
into default: and throws, even though the function already knows how to answer
that case: the ncols check right below the switch resolves an unrepresentable
shape to F16.

Route those types into that check instead of throwing. Only paths that abort
today change, so no quantization that currently succeeds is affected.

Found on a 4-wide depthwise conv kernel. llama-quantize reported nothing but
"failed to quantize model from ...", with no tensor name and no exception text,
which made a quant recipe that had simply not pinned the tensor look like a
corrupt model. It now names the tensor and continues.

* quantize: let --tensor-type name per_layer_token_embd

per_layer_token_embd shares the TOKEN_EMBD category with token_embd.weight, so
--token-embedding-type is returned for it before any --tensor-type pattern is
consulted, and there is no way to give it a tier of its own.

That grouping is fine as a default and stays the default. It is a poor fit for
the size, though: on qwen4exp the table is 97.7 GiB of a 337.6 GiB BF16 file and
about 46% of a 4-bit one, roughly eighty times token_embd.weight, and it is
read by ggml_get_rows rather than a matmul so no imatrix ever covers it.

Allow an explicit --tensor-type pattern to name it, and only it. Nothing
changes unless such a pattern is passed, and token_embd.weight keeps the old
precedence in either case.

Measured on Qwen3.8-Flash-Next, Q4_K_M with an imatrix: the table lands at q8_0
(51.9 GiB, 113.5 GiB total) by following --token-embedding-type, and pinning it
q4_1 gives 30.5 GiB for 92.1 GiB total, 19% off the file.

* quantize: size the output buffer exactly instead of nelements * 4

The per-tensor output buffer was sized `nelements * 4`, described as an upper
bound. It is a very loose one: the output is at most 2 bytes per element
(f16/bf16) and usually well under 1.1 (q8_0 and below), so between 2x and 4x of
it is never touched. The exact size is already known here, since it is what the
quantization loop writes, what new_size sums to, and what the GGUF metadata is
asserted against a few lines later.

On a model whose largest tensor is a few GB none of this matters. On
Qwen3.8-Flash-Next it does: per_layer_token_embd is 51.2 G elements, so the
buffer was 205 GB where 54 GB is needed at q8_0 and 32 GB at q4_1.

Measured on that model, VmHWM of a live llama-quantize was 485 GB per process.
Three of them fit in 2 TB and five did not, which is what an OOM-killed quant
ladder looks like. This removes about 150 GB of that.

Byte-identical output, verified against the same binary built at the parent
commit: q4_K, q8_0, q5_K, q6_K and IQ4_XS, over BF16 and F32 sources, with and
without a PLE table present. Six cases, six matching md5s.

* qwen4exp: hash the image placeholder for multimodal batches

The PLE row indices are computed host-side from ubatch->token, and set_input
returned early when that was null. A multimodal ubatch is exactly that case:
the mtmd layer consumes the image placeholder ids and hands llama_decode
embeddings instead. The early return left the I32 index tensor uninitialised,
so ggml_get_rows indexed a 320 M row table with whatever the buffer happened to
contain, and aborted:

  GGML_ASSERT(i01 >= 0 && i01 < ne01) failed
    ggml_compute_forward_get_rows
    mtmd_helper_decode_image_chunk -> llama_decode

Every image request crashed. Nothing caught it because the vision work had only
ever been verified by converting an mmproj, never by running one.

The reference computes the hash over input_ids, where those positions still
hold the image placeholder, so carry that id through as qwen4exp.ple.image_token_id
and hash it. The key is optional: a file converted before it existed falls back
to the PLE EOS token, which is defined and treats the image as a segment
boundary rather than crashing.

Verified end to end with llama-mtmd-cli, a Q4_K_M base and the F16 mmproj, on a
generated image with known content. The model names the red circle, the blue
square, the inverted green triangle and reads "UNSLOTH 42", each with the right
position.

* qwen4exp: support a non-unified KV cache in QSA

set_input_qsa asserted n_stream == 1, so llama-server could not serve this
model with more than one slot unless -kvu was passed. With a non-unified
cache each sequence owns its own cells, and a cell index means a different
token in each stream, so a single shared mapping is wrong.

- cell_blk, blk_cells and bias gain a stream dimension. At n_stream == 1
  these collapse to the shapes they had, so the unified path is unchanged.
- Scoring is now batched over streams. ggml_mul_mat matches ne[2] on both
  operands, so stream s's queries only ever meet stream s's blocks; without
  this sequences would score against each other's context.
- set_input_qsa loops per stream and resolves cells through
  v_cells[seq_to_stream[seq_id]], following set_input_kq_mask_impl, instead
  of hardcoding v_cells[0].
- llama_kv_cache_context::get_n_stream() is added, mirroring the ns that
  get_k and get_v already derive from the slot info.

build_attn_mask_top_k needed no change: it already expects
[n_top_k, n_batch, 1, n_stream], so the top-k result is reshaped to meet it.

set_input_qsa has exactly one caller, so the blast radius is qwen4exp only.

Validation, UD-Q4_K_XL on one B200:

- unified cache unchanged within noise: 1802.9/68.85 -> 1807.2/69.11 t/s at
  batch 1, 2262.5/192.43 -> 2270.1/193.75 at batch 4.
- non-unified now runs at npl 1, 4, 16 where it previously aborted, and is
  22% faster than the -kvu workaround at batch 16 (1205 vs 984 t/s total),
  since per-stream cells avoid the cross-sequence masking a unified cache
  pays for.
- no cross-stream contamination: four concurrent sequences each carrying a
  distinct secret all recall their own and no other, on both cache modes.
- test-llama-archs green on qwen4exp, deepseek2, gemma3n, qwen3next, llama.

Note on testing: comparing concurrent output against solo output exactly is
not a valid check. It failed 0/4 with no bug present, and the unified-cache
control failed the same way, because batch composition changes the
floating-point reduction order and near-tied tokens flip. The contamination
test above is what the exit code gates on.

* llama: keep the qwen4exp top-k attention mask arch-local

The QSA graph needed a build_attn that attends only to the cells named by a
top_k tensor, and the first version got it by adding a llm_graph_input_attn_kv
overload to llm_graph_context and factoring the mask construction out of the
existing MLA sparse path into a shared build_attn_mask_top_k.

That put a new arch on the shared attention path and made the deepseek32 and
glm-dsa attention build depend on a helper introduced for qwen4exp. Build the
mask in src/models/qwen4exp.cpp instead and leave llama-graph.{h,cpp} exactly as
they were: the MLA path keeps its own copy of the same node sequence.

The nodes emitted are unchanged, so this is bit-identical.

* llama: hold the qwen4exp indexer cache in a new llama_memory_hybrid_idx

The indexer key cache was added by extending llama_memory_hybrid with an
optional third cache, and the host-side cell/block mapping that drives QSA was
added as set_input_qsa on llama_kv_cache. Both are shared classes that every
hybrid and every attention model goes through.

Move both into a new memory type, llama_memory_hybrid_idx, following
llama_kv_cache_msa: the indexer cache and the pos<->cell translation live with
the sparse-attention memory rather than in the classes that serve every other
architecture. llama-kv-cache.{h,cpp} and llama-memory-hybrid.{h,cpp} are
restored to their unmodified state.

init_batch is repeated from llama_memory_hybrid because the indexer cache has to
be handed the attention cache's slot infos, and those are not reachable through
the context the base returns. Allocating them separately lets the two caches
drift, which is what pointed QSA's top-k at the wrong cells before.

The context derives from llama_memory_hybrid_context so build_inp_mem_hybrid
keeps working unchanged, and get_n_stream is computed from the slot infos
exactly as llama_kv_cache_context did.

Behaviour is unchanged: logits over an 8192-token sequence are bit-identical to
the previous implementation, sparse and dense alike.

* llama: save and restore the qwen4exp indexer KV cache

llama_memory_hybrid_idx forwarded clear, seq_rm, seq_cp, seq_keep, seq_add and
seq_div to the indexer cache but not state_write / state_read, so a saved
session dropped the indexer keys and a restored one selected QSA top-k against
an empty cache. The effect is invisible until the context passes
indexer_top_k + compress_ratio - 1 cells, because QSA is exactly dense below
that and the indexer contents cannot change the result.

The indexer section is written last rather than next to the attention cache it
mirrors. As a suffix, a reader that does not expect it stops early and the
trailing bytes are caught by the size check in state_load_file; placed between
the attention and recurrent sections it would instead be parsed as recurrent
state, which can succeed and restore silent garbage. It follows the same
LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY gate as the attention cache, since a partial
checkpoint deliberately skips the token-level attention caches.

The indexer restores its own cells instead of taking the attention cache's
restored slots. The two caches share size, padding and every sequence
operation, and init_batch hands the indexer the attention cache's slot infos,
so both state_read_meta calls run find_slot over identical occupancy and land
on identical cells.

The overrides live on llama_memory_hybrid_idx, the only memory type that owns
an indexer cache, so llama_memory_hybrid and every architecture that uses it
write and read exactly the bytes they did before.

The session and sequence state versions are bumped because the qwen4exp state
layout changed. The session path already rejects a short read via its size
check, but llama_state_seq_load_file accepts one silently, so only the version
check stops a pre-fix blob from being half-restored by a fixed build.

(cherry picked from commit 2721542354f8e158c3217625f4e2e7b83e51e3fe)

* llama: make the qwen4exp PLE n-gram history per context and serialise it

The PLE hash of a token mixes in the ple_ngram_size - 1 tokens before it, which
a decode ubatch does not carry, so they were remembered in a map on
llama_model_qwen4exp. That is the wrong owner twice over.

A llama_model is shared by every context that loads it, and the map was keyed
only by llama_seq_id, so two contexts running the same sequence id - two server
instances on one model, or a draft/target pair - overwrote each other's window.
The next_pos guard turned that into EOS padding instead of a crash, so it
degraded quality silently.

The map was also in no state blob: grep found ple_hist in neither
llama-kv-cache.cpp nor llama-memory-*.cpp nor llama-context.cpp. A restored
context therefore failed the next_pos check on its first ubatch and hashed the
first tokens after the restore against EOS padding. This is why a session blob
round-tripped byte for byte while the restored context computed different
logits: the state was never in the bytes.

It moves to llama_memory_hybrid_idx, which is per context, is the memory type
qwen4exp always builds, and already does the per-sequence bookkeeping this
needs. Every sequence operation now carries the window with it:

  seq_rm   a rewind (p1 < 0) truncates the window to the surviving prefix and
           moves next_pos to p0, so a rollback keeps exact context; a hole
           punched in the middle leaves the window non-contiguous, so it is
           dropped
  seq_cp   the destination inherits the source's window, truncated to the
           copied position range - a copied sequence continues with the same
           n-grams the source would have used
  seq_keep every other sequence's window is dropped, like its cells
  seq_add  a shift that moves the whole window keeps it and moves next_pos with
           it, which is the context-shift case; one that cuts through it drops
           it
  seq_div  positions stop being consecutive, so an overlapping window is
           dropped
  clear    everything is dropped

Dropping means next_pos = -1, which set_input turns into full EOS padding: the
same thing a fresh sequence gets, and the same thing this code did before it
followed the sequence operations at all, so no case is worse than before.

The state payload is a self-delimiting list, u32 count then per entry
{ i32 seq_id, i32 next_pos, u32 n_toks, i32 toks[n_toks] }, so a whole-context
save and a single-sequence save share one format and a single-sequence restore
can retarget the window at its destination seq_id. It is written after the
indexer section, last, for the same reason that one is: as a pure suffix an
older reader stops early instead of parsing these bytes as something else.

Unlike the indexer section it is not under LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY.
The window is recurrent state - it is the input the PLE convolution's own
recurrent state is derived from - and the recurrent cache beside it is written
for partial checkpoints too. Gating it would leave the server's speculative
decoding checkpoints restoring the conv state without the window that produced
it.

No further version bump: LLAMA_SESSION_VERSION 10 and LLAMA_STATE_SEQ_VERSION 3
were introduced for the indexer section in the same unreleased series, and both
changes are qwen4exp-only additions to the same blob layout.

Also fixes the padding of a short window. set_input pads a window shorter than
ngram_size - 1 up to that length, but prev() indexes the snapshot with the most
recent token last, and resize() pads at the back, so the filler EOS landed where
the immediately preceding token belongs. It now pads at the front. A window is
short at a sequence start after a one-token prefill, and after a seq_rm rewind,
which the new bookkeeping makes common.

Every architecture other than qwen4exp builds llama_memory_hybrid rather than
llama_memory_hybrid_idx, has no PLE table and never asks for a history, so
nothing about its graph, its sequence operations or its state bytes changes.

(cherry picked from commit de170364c052c68fcf63285cc0028095edb9f23c)

* qwen4exp: tidy comments and simplify image token read

Rewrite the comments this series adds to the AGENTS.md rules: one or two lines,
no prose hard-wrapped mid-sentence, no narrative or history, and no comment that
only restates the code. Net 146 fewer comment lines, no code change.

Correct the PLE image comment: mtmd does not consume the placeholder ids. An
image is decoded as an embeddings-only batch, so ubatch->token is null and the
per-position ids never exist here. gemma3n and gemma4 hit the same case and
stand in row 0 of per_layer_token_embd; qwen4exp stands in the configured image
token id instead.

Read image_token_id straight from self.hparams in the converter. base.py merges
text_config into the root of hparams, and the key sits at the root of
config.json, so the config.json re-read was redundant.

(cherry picked from commit 205840c12169057da3e8d2f65ec4ceec3e18b980)

* qwen4exp: support a quantized KV cache in the QSA attention path

(cherry picked from commit 4c30574f81dc1115d08078c47b6cf8c789c0a842)

* llama: give qwen4exp a large-graph node budget

(cherry picked from commit 37c8c194e6a30e4c46ac29bee3fb264f091596ef)

* qwen4exp: drop an unused variable that breaks -Werror builds

(cherry picked from commit 528d032b51fa3cf935ed3ef6e0fb1c7401df53b5)

* quantize: dequantize and quantize large tensors in row bands

f32_conv_buf held the whole dequantized tensor, which is 204.8 GB for
per_layer_token_embd alone and dies with std::bad_alloc long before the
work buffer is reached. Dequantize and quantize in bands of whole rows
instead, capping the f32 staging at 1 GiB per band.

Rows are independent and the imatrix is indexed by column, so band
boundaries cannot change any output byte. Bands nest inside the existing
per-expert loop so each expert slice keeps its own imatrix, and a band is
kept to at least one quantization chunk per worker thread so the existing
multithreading still has work. F32 sources still stage nothing and are
banded by pointer arithmetic into the tensor.

llama_tensor_dequantize_impl now takes a first element offset; the single
caller is updated.

(cherry picked from commit 658c22549613555dbce57a772be4de8509eba3ee)

* llama: segment the qwen4exp fused QKV for tensor split

qwen4exp was missing from the gated delta net branch of get_split_segments,
so its attn_qkv.weight, shaped {n_embd, 2*key_dim + value_dim}, fell through
to the generic fused QKV rule and tripped
GGML_ASSERT(tensor->ne[axis] == n_embd + 2*n_embd_gqa) while loading with
--split-mode tensor. --split-mode layer was unaffected.

qwen4exp broadcasts K to the V heads by tiling, k_conv is grown with a plain
ggml_repeat_4d over the head axis so that v head j pairs with k head
j % n_k_heads. That is the Qwen 3.5 pattern, not the repeat interleave that
Qwen 3 Next builds explicitly, so qwen4exp takes the else branch and its V is
segmented on the scale of K.

Reported by benklop.

(cherry picked from commit 353d753f595dc81634ae6130188b31f06018f5ae)

* llama: fix the qwen4exp PLE history seq_rm(-1) iterator invalidation and the fatal-warning build

ple_hist_rm recursed over ple_hist with a range-based for and the recursive call
erases the entry it is iterating when the whole sequence is removed (p0 <= 0,
p1 < 0), so the loop then increments an invalidated iterator. It is unreachable
today only because llama_memory_recurrent::seq_rm rejects seq_id < 0 before
llama_memory_hybrid_idx::seq_rm reaches the history, which is a guard in another
class. Advance past the entry before recursing.

Two smaller things in the same area:

  - the n_toks sanity bound in ple_hist_state_read was the literal 64, which is
    the value of LLAMA_MAX_PLE_HEADS, not of the quantity being checked. The
    window is at most ple_ngram_size - 1 tokens, so the bound is
    LLAMA_MAX_PLE_NGRAM - 1, eight times tighter.

  - build_conv_state_at left mem_size unused, so -DLLAMA_FATAL_WARNINGS=ON does
    not compile. Predates this series; drop the line.

(cherry picked from commit 6eba44a89d5f328eb4859b844e1d28fb564cbe3e)

* qwen4exp: include llama-impl.h explicitly for llama_mul_mat_hadamard

(cherry picked from commit b634fd4d250d181ef82bf78bd00c1ae3b96a7af6)

* convert: fix the qwen4exp lint and type-check failures

flake8 flagged an unused MmprojModel import, and ty flagged seven errors in
the PLE streaming path: eos_token_id can be absent, and _ple_map, _ple_path,
_ple_row_dim and _ple_rows_per_shard are all Optional at the declaration but
were dereferenced without narrowing.

The map is opened and the stride fixed before the first shard is written, and
_finish_ple_table only runs once every shard has landed, so the invariants
hold. Assert them so the checker can see it. A missing eos_token_id now raises
with the reason instead of a TypeError from int(None).

* llama: give the qwen4exp indexer cache its own tensor names

The indexer KV cache and the attention KV cache both named their tensors
cache_k_l%d, so the Meta backend matched the indexer cache against the
attention split pattern and aborted in handle_set_rows. Tag the names
instead, and mirror the indexer cache: it has one key head and its
projections are mirrored.

(cherry picked from commit a1cdc8181134659766763a17762545a1f0e5db7b)

* qwen4exp: double the Q split granularity for tensor parallelism

qwen4exp fuses the attention gate into attn_q.weight the same way qwen3next
and qwen 3.5 do, so a device boundary must fall on a whole q+gate pair or the
Q heads stop lining up with the K/V heads and attn_output rows.

(cherry picked from commit 6c9a592f0a425a459ab6efae3b897cf68460e244)

* qwen4exp: keep the indexer cache in step across server slots

The QSA indexer keeps a side cache addressed by the cells of the attention
cache, so cell j has to hold the same token in both: the top-k indices it
produces are applied to the attention KQ mask. init_batch already hands the
indexer the attention cache's slot layout rather than letting it look for its
own, but the restore path did not. state_read called llama_kv_cache::state_read
on the two caches in turn and each ran its own find_slot over its own occupancy.
That agrees only for as long as nothing has already pushed the two caches apart,
which is the property a restore is supposed to re-establish rather than one it
can lean on.

The failure path was the worse half, and it is reachable from the public API
with nothing more than a short buffer. Truncating a good blob at 35 offsets and
feeding it to llama_state_seq_set_data left the two caches disagreeing at 5 of
them, and every one of 23 truncations of a whole-context blob did. Four of those
five land inside the attention section, so the attention cache drops the
sequence and the indexer keeps it; only the cut that lands in the indexer
section gives the opposite direction. llama_kv_cache::state_read cleans up its
own cache and rethrows, so whichever way it falls, nothing is left to bring the
two back together. The server papers over this by clearing the slot when a
prompt cache load fails; a caller of llama_state_seq_set_data that does not is
left with an indexer addressing cells that no longer mean what it thinks.

llama_kv_cache::state_read_sinfo reports the cells a restore landed in, or takes
a copy of them, and state_read_meta uses a supplied layout in place of find_slot
once it has checked that those cells are free here too. The indexer now adopts
the attention cache's restored layout by construction instead of reproducing it
by coincidence, and a layout that does not fit fails the read rather than being
applied over cells that already drifted. The hybrid restore is wrapped so that
any failure drops the sequence, or for a whole-context restore the context, from
all three caches at once, which is a state they do agree on.

* kv-cache: clear the cache once when restoring a whole context

state_read walks the streams of the cache in turn, and for a whole-context restore
each stream went through state_read_meta, which starts by calling clear(). clear()
resets every stream at once, so each stream after the first threw away the streams
already restored, and the K/V buffers with them. A non-unified cache holds one
stream per sequence, so a context saved with N sequences in it came back with only
the sequence in the last stream that carried any cells - the highest sequence id.
A unified cache has one stream and never showed it.

The cache is now emptied once, before the loop, which is what a whole-context
restore means. A blob whose streams are all empty now empties the cache as well,
where before it left the old contents in place.

* kv-cache: check the mirrored slot layout on a whole-context restore too

state_read_meta only looked at the layout it was given on the single-sequence path.
A whole-context restore lays the cells out from 0 in both caches, so they agree as
long as they restore the same number of cells, but nothing checked that they did: an
indexer section belonging to some other context was read over cells the attention
cache had filled from a different one, which is the state the indexer must never be
left in.

* qwen4exp: give the PLE conv history its own mirrored recurrent row

n_embd_r() reserved n_conv + ple_conv_state() so that one cache_r_l row could
carry both the delta-net conv state and the PLE dilated conv history, but the
QWEN4EXP arm of get_split_segments only described n_conv. Under -sm tensor the
segment sum came up short by ple_conv_state() and llama_memory_recurrent
construction aborted in ggml_backend_meta_alloc_ctx_tensors_from_buft.

Widening the segment list is not the fix. The Meta backend propagates a view's
split descriptor from its parent unchanged, so a view of one sub-range of a
split axis is sized as the whole row on every device; declaring the PLE tail as
a second segment merely moves the abort to "shape mismatch for VIEW" at graph
allocation. The two histories also want opposite policies: the delta-net state
is split by head to match wqkv and ssm_conv1d, while per_layer_tok_embd,
ple_conv1d and ple_norm_conv are all mirrored, so every device computes the
whole dilated conv and needs the whole history. One tensor cannot be both, and
the split state has no per-segment mirroring.

Move the PLE history into its own cache_ple_r_l%d row, mark it MIRRORED, and
return n_embd_r() to n_conv. The row is allocated only on layers where is_ple
holds, so mirroring one 92160-element row per device replaces a 92160-element
tail on all 36 recurrent rows: the recurrent R footprint drops rather than
grows. build_conv_state_at now takes its width from the tensor it was handed
and keys its gather on that tensor, which also drops a cont of a strided view.

* no more ple_hist (use master version)

* llama: give the qwen4exp full memory context its indexer cache

graph_reserve() walks a full memory context, and qwen4exp builds its
sparse attention only when the context exposes an indexer cache. the
full-context constructor left ctx_idx null, so the reserved worst case
was the dense fallback: a smaller graph than the one decode executes.
ggml-alloc then had to grow the compute buffer on the first decode,
past the size reported at load.

with -np 4 -c 32768 -fa on -ctk q8_0 -ctv q8_0 on an IQ1_S qwen4exp,
the reserved CUDA0 buffer was 217.00 MiB against 275.71 MiB actually
used, and CUDA_Host 42.31 MiB against 191.14 MiB. reserving the sparse
graph makes both match exactly, in unified and non-unified cache mode.

Co-authored-by: Pascal <admin@serveurperso.com>
Assisted-by: Claude

* qwen4exp: shrink the PLE hparams storage

llama_hparams is held by value inside llm_graph_params and every llm_graph_input_*,
and llm_graph_params is a stack local in graph_reserve and process_ubatch, so its
width is paid on every worker thread stack.

is_ple_impl spent 2048 bytes carrying 512 bits. It is the one per-layer flag that is
not moved through the loader's uint32 array templates, so a bitset costs nothing in
call sites and also removes the uninitialized read that non-qwen4exp archs had, since
nothing filled the array for them.

The PLE head offsets and vocab sizes are token-space indices; the gather that consumes
them already truncates to int32, so 64-bit storage was never reachable. The gguf arrays
stay uint64 for file compatibility and are narrowed on load.

sizeof(llama_hparams) 34440 -> 31944, sizeof(llm_graph_params) 34872 -> 32376.

* llama: opt-in random-access mmap advice for host-resident gather tables

qwen4exp keeps per_layer_token_embd on the host: 26.8 GiB at IQ4_NL, read
by ggml_get_rows as 16 gathers of ~90-170 bytes per token, spread across
16 head regions ~20M rows apart. Measured over 4.75M gathers, no two
consecutive gathers land on the same 4 KiB page, so the readahead the
loader asks for buys nothing here and the whole table ends up cached to
serve about 4% of itself.

llama_mmap applies POSIX_FADV_SEQUENTIAL, MAP_POPULATE and a whole-file
POSIX_MADV_WILLNEED unconditionally. Those are right for streaming the
file once into buffers and wrong for whatever stays mapped afterwards.

Under LLAMA_MMAP_RANDOM the eager pull-in is skipped and the mapping is
advised random once every tensor has been read, so the load itself keeps
its sequential readahead. That alone drops the table to 4.4% resident but
serializes one NVMe latency per gather.

The second half is what pays for it: the PLE input already computes every
row index for the ubatch before the graph runs, so the pages those rows
fall on are handed to the kernel in one batch and the reads overlap.
POSIX_MADV_WILLNEED on POSIX, PrefetchVirtualMemory on Windows, which
takes the discontiguous ranges in a single call.

Off by default and off for every other model: the batched prefetch keys
off "this mapping was advised random", which nothing sets unless the user
opts in.

  -c 512 --chunks 60, cold, IQ1_S, mean of 3:

    default            35.3 s   26.82 GiB resident (100%)
    advice only       104.5 s    1.19 GiB resident (4.4%)
    advice + prefetch  34.2 s    1.19 GiB resident (4.4%)

  PPL 4.2346 +/- 0.07862 in all three. IQ1_S KLD is unchanged in every
  field, including Mean KLD 0.396070 +/- 0.001931 and Same top p 77.325%.

* llama: narrow the random-access mmap advice to the gather table

The advice was applied per mapping: every mapping the model kept got
POSIX_MADV_RANDOM plus a whole-file POSIX_FADV_RANDOM, and the eager
pull-in was skipped for every file. On qwen4exp that also hit
token_embd.weight, which sits 0.33 GiB past the PLE table in the same
shard and is read densely, not by sparse gathers. Measured over
-c 512 --chunks 60 on IQ1_S it fell to 8.45% resident, against 100% with
the feature off.

A model now nominates its gather tables (qwen4exp: per_layer_tok_embd)
and only those byte ranges are advised. The range is rounded out to
whole pages, which on this model takes in 832 bytes before and 192
after. token_embd goes back to 86.55% resident and the PLE table still
drops to 4.44%; smaps shows one VM_RAND_READ VMA of exactly the table
instead of one over all 27.16 GiB that stays mapped.

posix_fadvise is dropped from the narrowed path. POSIX_FADV_RANDOM
ignores its offset and length and marks the whole open file, and the
FMODE_RANDOM it sets is only read by page_cache_sync_ra() on the read()
path, which a fault on a MADV_RANDOM vma never reaches. POSIX_FADV_
DONTNEED does take a range, so the drop mode keeps it.

The eager pull-in is now skipped only for the files holding a nominated
table, and re-issued as WILLNEED over the rest of such a file, so other
shards load exactly as before.

prefetch_rows() keys off the tensor being nominated rather than off a
mapping-level flag, so the batched readahead lands only where the advice
did.

  -c 512 --chunks 60, cold, IQ1_S, mean of 3, total wall:

    default              32.50 s
    whole mapping        30.05 s
    narrowed             30.35 s

  PPL 4.2061 in all three. IQ1_S KLD is bit-identical with the feature on
  and off, including Mean KLD 0.396070 +/- 0.001931 and Same top p
  77.325%. tg128 73.65 +/- 0.33 narrowed against 73.49 +/- 0.34 whole.

Assisted-by: Claude

* llama: fold the random-access prefetch into its own feature flag

LLAMA_MMAP_RANDOM_PREFETCH existed to measure the two halves of the feature
apart, and the measurement is done: on a cold cache over the same wikitext
run, MADV_RANDOM without the batched readahead takes 94.4 s against 36.7 s
for an untouched mapping, while the pair together take 34.1 s. Suppressing
the kernel's readahead only pays if we replace it, so the split let a user
select a 2.6x regression through a documented switch.

Keep the accessor, since the call site reads better than a mode comparison,
but derive it from the mode alone.

* FACP (Fewer Acronym Classes Please)

* qwen4exp: bias the QSA selection per block, not per cell

The QSA bias is a graph input, so it is pinned on the host and uploaded every
decode, and at -c 32768 -np 4 its twelve copies were 768 of the 815 MiB of
reserved host compute buffer.

Only one half of it needs a cell: whether the cell sits in the always-visible
tail, and whether its block was pooled. Both are properties of the block. The
other half - empty, other sequence, or in the future - is the plain visible/not
test the attention mask already carries over the same cells, so add that mask
instead of repeating it. The bias then holds one value per block.

A block sits wholly inside or wholly outside the tail because the tail starts on
a block boundary, so one value per block is exact. Cells no block covers keep
their -inf from the mask.

The mask is F16 and the bias F32, and a mixed ggml_add reinterprets the F16
buffer as float rather than converting it, so the cast is required.

reserved host compute buffer at -c 32768 -np 4:
  --kv-unified      814.86 -> 238.86 MiB, CUDA0 721.07 -> 421.07 MiB
  --no-kv-unified   214.86 ->  70.86 MiB, CUDA0 317.07 -> 265.07 MiB

Selection is unchanged: over 8192 tokens, four times the budget, every QSA
layer returns identical top-k indices and the logprobs are bitwise equal.

Two things a reviewer should know. A cell whose position divides past the last
block is guarded by an assert rather than handled, because no run reached it.
And the mask's same-position M-RoPE rule cannot fire for text and was never
exercised for images, so the 2D case is unverified.

* clean up code comments

* clean up new comments

* revert LLAMA_MMAP_RANDOM

* nits

* replace some changes with #27795

* improve the m-rope image for get_prev_tokens

* LazyChunkedTensor

* fix lint

* add some validations

* reduce input nodes

* trim output tokens

* nits

* some more sanity checks

* fix llm_graph_input_ple reuse

* exclude from webgpu test

---------

Co-authored-by: danielhanchen <danielhanchen@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Co-authored-by: Pascal <admin@serveurperso.com>
Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
2026-08-27 21:32:31 +02:00
Slobodan JosicandGitHub 6fdd0ac890 ci : bundle HIP runtime DLLs with Windows ROCm release (#26973)
Copy amdhip64_7, amd_comgr and rocm_kpack next to the binaries so the correct
HIP runtime loads over the driver's copy in System32. Fixes #26929.
2026-08-27 19:27:57 +02:00
b10f9ca58c spec : add DFlash2 support (local convolution + candidate selector) (#27342) (#27816)
* spec : add DFlash2 support (local convolution + candidate selector) (#27342)

* support DFlash2

* Add p_min in DFlash2

Assisted-by: Claude Opus 5

* Revert unnecessary changes

Assisted-by: Claude Opus 5

* Revert draft sampling in rejection sampling

Assisted-by: Claude Opus 5

* Refactor code structure

Assisted-by: Claude Opus 5

* Delete embedding scaling

Assisted-by: Claude Opus 5

* Gate output transforms on DFlash2

Assisted-by: Claude Opus 5

* Optimize Dflash 2 cost

Assisted-by: Claude Opus 5

* Avoid using atoi

Assisted-by: Claude Opus 5

* Modify comments

Assisted-by: Claude Opus 5

* Move llama_model_dflash_selector_top_k to llama-ext.h

Assisted-by: Claude Opus 5

* Formatting

Assisted-by: Claude Opus 5

* Apply patch to fix the mrope bug

Assisted-by: Claude Opus 5

* fix ci

Assisted-by: Claude Opus 5

* Fix graph number calculation

Assisted-by: Claude Opus 5

* rename hid and unary

Assisted-by: Claude Opus 5

---------

Co-authored-by: Jian Chen <jianchen0311@gmail.com>
Co-authored-by: Xuan-Son Nguyen <son@huggingface.co>

* revert top-k.cu changes

---------

Co-authored-by: Zihan Zhang <tiancaizhangdaxian@sjtu.edu.cn>
Co-authored-by: Jian Chen <jianchen0311@gmail.com>
2026-08-27 19:17:07 +02:00
Shawn GuandGitHub 58546250cf opencl: add bin kernels kernel_gemm_moe_q4_0_q8_1_dp4a_bin, kernel_gemm_moe_mxfp4_q8_1_dp4a_bin (#27768) 2026-08-27 09:44:05 -07:00
Xuan-Son NguyenandGitHub 732707dff2 quantize: cap working memory size to avoid loading big tensors onto RAM (#27795) 2026-08-27 18:31:13 +02:00
ShobhitandGitHub cb300598d5 Feature: Added LIGHTNING_INDEXER support for Deepseek V4 ops on Vulkan Backend (#27453)
* vulkan: add LIGHTNING_INDEXER op

* vulkan: updated lightning_indexer.comp and ggml-vulkan.cpp with 128-lane dot-product reduction moved from a shared-memory tree to subgroupAdd.

* vulkan: cleanup; Skip bounds checks

* vulkan: cleanup FA_K_ONLY

* Revert "vulkan: cleanup FA_K_ONLY"

This reverts commit fdcbdd9151.

* vulkan: restore interleaved K/V buffer ordering

* vulkan: Remove FA_K_ONLY

* vulkan: Revert flash_attn_dequant

* vulkan: Revert tests in backend-ops.cpp
2026-08-27 15:34:42 +02:00
Sigbjørn SkjæretandGitHub 1a946ec745 pr2wt : use ssh/https remote in worktree depending on base (#27800) 2026-08-27 16:27:17 +03:00
Xuan-Son NguyenandGitHub fac889fb38 llama: model_loader: add TENSOR_READ_LAZY (#27794)
* llama: model_loader: add TENSOR_GET_ROW_LAZY

* add --tensor-read-lazy

* rename to TENSOR_READ_LAZY

* gen docs

* address comments
2026-08-27 15:14:34 +02:00
Aleksander GrygierandGitHub cae63579b6 ui: Improve Chat Form Actions UI/UX (models selector, add panel) (#27746)
* ui : strip trailing container-format segments from parsed model names

* ui : show reasoning and modality icons on model options and search by modality

* ui : keep reasoning submenu visible regardless of model state

* ui : add show-org-name-in-trigger display setting

* ui : move model list into a submenu within the model selector

* ui : make model option hover and focus highlight override the active state

* ui : add raw model id tooltip to model selector options

* feat: Enable microphone input as default for audio models

* ui : fix eslint issues in chat form and model selector

* ui: show modality icons instead of file submenu in chat add menu

Assisted-by: pi

* chore: Format

* chore: Format

* ui: add ModelCapability enum and shared modality/capability icon constants

Assisted by: pi:GLM-5.3-Flash

* ui: derive modality badge icons and labels from shared constants

Assisted by: pi:GLM-5.3-Flash

* ui: split model option icons into capabilities and modalities

Replace the supportsThinking flag on ModelId with a capabilities object
keyed like ModelModalities, so future capabilities (tool calls, etc.)
slot in alongside reasoning. Icons and labels now come from the shared
CAPABILITY_ICONS/MODALITY_ICONS constants.

Assisted by: pi:GLM-5.3-Flash
2026-08-27 14:47:36 +02:00
Kartik GuliaandGitHub bcb6084a4e convert : fix Nemotron-H LoRA GGUF conversion (#27356)
* convert: fix Nemotron-H LoRA GGUF conversion

* Removed redundant JSON import.
2026-08-27 14:41:24 +02:00
Aleksander GrygierandGitHub fe235f4343 ui: Replace per-conversation MCP overrides with per-conversation tool policy (#27745)
* ui: replace per-conversation MCP overrides with per-conversation tool policy

MCP server enabled state is now global (server.enabled); per-conversation
control moves to disabled tool keys and categories seeded into each new
conversation. Aligns the add sheet with the dropdown options and flattens
MCP tool groups in the tools submenu.

Assisted-by: pi

* ui: keep tool policy migration running when defaults parse fails

A corrupt disabledToolKeys localStorage entry no longer aborts the
migration; it falls through with empty defaults so legacy MCP server
overrides still get converted.

Assisted-by: pi

* ui: fall back to global defaults when agentic flow has no tool policy

Passing empty disabled sets bypassed the global defaults and could
enable tools for callers that do not pass a policy yet.

Assisted-by: pi

* ui: align preferences section headers with their methods

The Reasoning Effort and Working Directory headers sat above tool
policy methods; move them above setCwd and setReasoningEffort. Also
clarify the disabled tools JSDoc: existing rows with an unset field
have an empty policy, defaults apply only when there is no active
conversation.

Assisted-by: pi

* ui: gate MCP server avatars on conversation tool policy

Servers whose tools are disabled for the current conversation (MCP
category or server-scoped key) no longer show as enabled for the chat.

Assisted-by: pi

* ui: drop unused MCP category toggle from tools panel hook

Per-conversation MCP control is server-granular; no component renders
a whole-category toggle, so remove the dead API.

Assisted-by: pi

* ui: skip MCP init when flow policy disables the MCP category

Resolve the effective tool policy before deciding whether to
initialize MCP so flows that will not send any MCP tools skip the
init work. Callers without a policy keep falling back to global
defaults.

Assisted-by: pi

* chore: format

* ui: restore reasoning section in mobile add sheet

The sheet rewrite dropped it; the desktop dropdown still has it.
MCP Prompts and Resources stay out of the sheet on purpose.

Assisted-by: pi

* ui: clear MCP server group key in enableAllToolsForServer

The group key disables every tool of the server regardless of
per-tool keys, so re-enabling a server from Settings did nothing
while it was set.

Assisted-by: pi

* ui: skip MCP init when no policy-enabled server remains

Extends the category-level check: the flow also skips MCP init when
every globally-enabled server has its server-scoped group key
disabled in the tool policy.

Assisted-by: pi

* ui: make Settings tools tab edit defaults with category toggles

Adds per-category checkboxes and a caption stating the tab applies
to new conversations; tool picks inside a chat only affect that
chat.

Assisted-by: pi

* ui: gate cwd picker and mention picker on effective tool policy

Both checked the global disabled set directly, so a conversation
that disabled file_search still showed search as available.

Assisted-by: pi

* ui: clean up tool key helpers and store docs

Documents getEnabledToolsForLLM properly, unstacks the JSDoc at
isEntryEnabled, makes setToolEnabled persist like setCategoryEnabled
(toggleTool now delegates to it), and routes the serverId-less MCP
branch of toolKey through getMcpServerToolsKey so both key formats
come from one place. Preferences banner comments become plain
comments so they no longer read as class member docs.

Assisted-by: pi

* ui: indeterminate group checkboxes and inert grayed rows

A category that is on with nothing enabled under it now shows the
mixed checkbox state instead of a checked box next to 0/N. Rows
grayed out by a disabled parent no longer stay clickable behind
opacity.

Assisted-by: pi

* ui: gate MCP prompt and resource capabilities on tool policy

hasPromptsCapability and hasResourcesCapability accept an optional
set of usable server ids; ChatFormActions resolves it from global
enablement minus the active conversation's policy. Restores the
per-chat gating the old mcpServerOverrides provided; callers without
arguments keep global behavior.

Assisted-by: pi

* ui: remove unmounted MCP submenu component

Never rendered anywhere; its entries are duplicates (prompts and
resources live in the attachment menu, servers in the add menu and
sheet) that would need capability wiring maintained for nothing.

Assisted-by: pi

* ui: fix model information dialog width on all screen sizes

The dialog sets container-type: inline-size, so auto width ignores
its contents and collapses to padding. Give it an explicit viewport
width on mobile and cap at 60rem on desktop.

Assisted-by: pi

* ui: scroll wide chat template in model information dialog

Long unbreakable Jinja tokens blew out the table and dialog width;
the block now scrolls horizontally instead of stretching.

Assisted-by: pi

* ui: use fixed table layout in model information dialog

Auto table layout sizes columns to content min-content, so the chat
template's long lines kept inflating the dialog despite the scroll
wrapper. Fixed layout pins the first column and gives the value
column a definite width the wrapper can scroll within. min-w-0 on
the grid item guards the same path on the grid side.

Assisted-by: pi

* ui: make model information dialog full-screen on mobile

Matches the settings dialog pattern: full viewport below md,
calc-sized and capped at 60rem on desktop.

Assisted-by: pi

* ui: stack chat template row in model information dialog

Label above the block in a single full-width cell, so the template
gets the whole table width and its horizontal scroll is usable on
narrow screens.

Assisted-by: pi

* ui: scroll model information header with the content

The base dialog header is sticky; this dialog overrides it to
relative so the title and description scroll away with the body.
relative keeps the header as the close button's containing block.

Assisted-by: pi

* ui: replace literal comment text in sheet group snippet

A // line inside the Svelte snippet rendered as visible text; use an
HTML comment.

Assisted-by: pi

* ui: let indeterminate state win over checked in group checkboxes

The checkbox indicator snippet renders the check icon whenever
checked, so the mixed state never showed. Pass the checked prop
as false while indeterminate.

Assisted-by: pi

* ui: initialize only policy-enabled MCP servers for a flow

ensureInitialized accepts an optional server id set; the agentic
flow passes the servers its tool policy leaves usable, so servers
disabled for the conversation no longer get connected. Callers
without arguments keep the global behavior.

Assisted-by: pi

* ui: derive group checkbox state in useToolsPanel

Moves the mixed-state derivation out of the submenu and sheet
snippets into one getGroupCheckState accessor; the snippets just
consume checked and indeterminate.

Assisted-by: pi

* ui: gate /prompt command on the conversation tool policy

The slash command's availability now follows the same rule as the
agentic flow instead of the global capability check, so it disables
itself when the conversation's policy leaves no usable MCP server.

Assisted-by: pi

* ui: remove dead MCP prompt menu trigger chain

The /prompt slash command is the surviving trigger; the menu-button
path (onMcpPromptClick, hasMcpPromptsSupport, showMcpPromptButton,
the MCP_PROMPT attachment item and its unrendered item arrays) has
no consumer left. Message display for inserted prompts is untouched.

Assisted-by: pi

* ui: render dash for mixed-state group checkboxes

The accessor refactor dropped the checked-and-not-indeterminate
guard, so the category-on flag won and the dash never showed. The
tooltip keeps using the raw parent flag since clicking a mixed
group still disables it.

Assisted-by: pi

* ui: fix group checkbox sticking checked after disable

Clicking a mixed-state group box let bits-ui optimistically flip
its internal checked flag; the derived checked prop did not change
across the transition (both mixed and off map to checked=false),
so Svelte never applied the settled value and the check icon stuck
while the count already read 0/7.

Pass the parent flag as checked and the mix as indeterminate, so
every group toggle changes checked; render the dash on top of a
checked box for the mixed state.

Assisted-by: pi

* fix: UI for Model Information dialog

* ui: keep MCP connections stable across policy switches

ensureInitialized folds the policy into its config signature, so
alternating two conversations with different policies tore down and
reconnected every server with health checks included. Tool collection
already filters by the flow policy, so initialize every
settings-enabled server instead and never pass a policy into the MCP
config. The duplicated policy-server check becomes one accessor on
ConversationPreferences.

Assisted-by: pi

* ui: remove dead MCP resources menu trigger chain

Same shape as the earlier prompt trigger cleanup: nothing renders the
MCP resources menu button, and the only live entry into resource
browsing is Settings > MCP Servers plus the attachment resource
picker. Drop onMcpResourcesClick, hasMcpResourcesSupport,
MCP_RESOURCES_CLICK, the AttachmentItemVisibleWhen enum and
hasResourcesCapability; the resources display, browser and picker
components are untouched.

Assisted-by: pi
2026-08-27 13:08:01 +02:00
Gaurav GargandGitHub 2bb9bddafa spec: Add benchmark-only synthetic speculative acceptance options (#27711)
* Add benchmark-only synthetic speculative acceptance to llama-server and llama-cli

* Address review comments

* Address review comments

* Add some comments in the code
2026-08-27 13:53:42 +03:00
deae5ee133 model : simplify MiniMax-01 graph (#27790)
Co-authored-by: Stanisław Szymczyk <sszymczy@gmail.com>
2026-08-27 13:27:52 +03:00
152 changed files with 7907 additions and 1876 deletions
+6 -6
View File
@@ -1,12 +1,12 @@
ARG OPENVINO_VERSION_MAJOR=2026.3
ARG OPENVINO_VERSION_FULL=2026.3.0.22451.bd8d6542e3c
ARG OPENVINO_VERSION_MAJOR=2026.3.1
ARG OPENVINO_VERSION_FULL=2026.3.1.22476.56d9685302d
ARG UBUNTU_VERSION=24.04
# Intel GPU driver versions. https://github.com/intel/compute-runtime/releases
ARG IGC_VERSION=v2.38.2
ARG IGC_VERSION_FULL=2_2.38.2+22051
ARG COMPUTE_RUNTIME_VERSION=26.27.39122.11
ARG COMPUTE_RUNTIME_VERSION_FULL=26.27.39122.11-0
ARG IGC_VERSION=v2.40.13
ARG IGC_VERSION_FULL=2_2.40.13+22418
ARG COMPUTE_RUNTIME_VERSION=26.31.39395.13
ARG COMPUTE_RUNTIME_VERSION_FULL=26.31.39395.13-0
ARG IGDGMM_VERSION=22.10.0
# Intel NPU driver versions. https://github.com/intel/linux-npu-driver/releases
+4 -4
View File
@@ -41,8 +41,8 @@ jobs:
env:
# Sync versions in build-openvino.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Clone
@@ -69,8 +69,8 @@ jobs:
env:
# Sync versions in build.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Clone
+9 -10
View File
@@ -32,6 +32,8 @@ env:
LLAMA_ARG_LOG_COLORS: 1
LLAMA_ARG_LOG_PREFIX: 1
LLAMA_ARG_LOG_TIMESTAMPS: 1
# TODO: fix and re-enable the `test-llama-archs` and `test-recurrent-state-rollback`
CTEST_EXCLUDE: "test-llama-archs|^test-recurrent-state-rollback"
jobs:
ubuntu-24-openvino:
@@ -39,8 +41,8 @@ jobs:
env:
# Sync versions in build-openvino.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Clone
@@ -78,26 +80,24 @@ jobs:
- name: Test (CPU)
id: cmake_test_cpu
# TODO: fix and re-enable the `test-llama-archs` test below
run: |
cd ${{ github.workspace }}
ctest --test-dir build/ReleaseOV -L main -E "test-llama-archs|test-recurrent-state-rollback-nemotron-h" --verbose --timeout 2000
ctest --test-dir build/ReleaseOV -L main -E "${{ env.CTEST_EXCLUDE }}" --verbose --timeout 3000
- name: Test (GPU)
id: cmake_test_gpu
# TODO: fix and re-enable the `test-llama-archs` test below
run: |
cd ${{ github.workspace }}
export GGML_OPENVINO_DEVICE=GPU
ctest --test-dir build/ReleaseOV -L main -E "test-llama-archs|test-recurrent-state-rollback-nemotron-h" --verbose --timeout 3000
ctest --test-dir build/ReleaseOV -L main -E "${{ env.CTEST_EXCLUDE }}" --verbose --timeout 3000
openvino-windows-2022:
runs-on: windows-2022
env:
# Sync versions in build-openvino.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Clone
@@ -159,14 +159,13 @@ jobs:
- name: Test (CPU)
id: cmake_test_cpu
shell: cmd
# TODO: fix and re-enable the `test-llama-archs` test below
run: |
REM Find extracted OpenVINO folder dynamically
for /d %%i in (openvino_toolkit\*) do set OPENVINO_ROOT=%%i
call "%OPENVINO_ROOT%\setupvars.bat"
cd build
ctest --test-dir ReleaseOV -L main -E "test-llama-archs|test-recurrent-state-rollback-nemotron-h" -C Release --verbose --timeout 3000
ctest --test-dir ReleaseOV -L main -E "${{ env.CTEST_EXCLUDE }}" -C Release --verbose --timeout 3000
- name: ccache-clear
uses: ./.github/actions/ccache-clear
+2 -2
View File
@@ -288,8 +288,8 @@ jobs:
env:
# Sync versions in build.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Clone
+49 -26
View File
@@ -415,8 +415,8 @@ jobs:
env:
# Sync versions in build-openvino.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Set OpenVINO version output
@@ -529,8 +529,8 @@ jobs:
env:
# Sync versions in build-openvino.yml, build-self-hosted.yml, release.yml, build-cache.yml, .devops/openvino.Dockerfile
OPENVINO_VERSION_MAJOR: "2026.3"
OPENVINO_VERSION_FULL: "2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR: "2026.3.1"
OPENVINO_VERSION_FULL: "2026.3.1.22476.56d9685302d"
steps:
- name: Set OpenVINO version output
@@ -714,10 +714,10 @@ jobs:
with:
key: release-windows-2025-vs2026-${{ matrix.arch }}-cpu
# TODO: build only the ggml-hip backend like the other windows backend jobs
# (windows-cuda, windows-sycl), then drop the ui-build dependency
# note: builds only the ggml-hip backend - llama-server is injected from the
# windows-cpu zip during the release "Merge artifacts" step
windows-rocm:
needs: [check-release, ui-build]
needs: [check-release]
if: ${{ needs.check-release.outputs.should_release == 'true' }}
runs-on: windows-2022
@@ -736,11 +736,9 @@ jobs:
with:
fetch-depth: 0
- name: Download UI build
uses: actions/download-artifact@v7
with:
name: llama-ui.zip
path: tools/ui/dist
- name: Install Ninja
run: |
choco install ninja
- name: ccache
uses: ggml-org/ccache-action@v1.2.21
@@ -795,33 +793,28 @@ jobs:
- name: Build
run: |
mkdir build
cd build
cmake .. `
-G "Unix Makefiles" `
cmake -S . -B build `
-G "Ninja Multi-Config" `
-DCMAKE_PREFIX_PATH="${env:HIP_PATH}" `
-DCMAKE_BUILD_TYPE=Release `
-DGGML_BACKEND_DL=ON `
-DGGML_NATIVE=OFF `
-DGGML_CPU=ON `
-DGGML_CPU_ALL_VARIANTS=ON `
-DGGML_CPU=OFF `
-DGGML_HIP=ON `
-DCMAKE_C_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang.exe" `
-DCMAKE_CXX_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang++.exe" `
-DCMAKE_C_FLAGS="-Wno-error=incompatible-pointer-types" `
-DCMAKE_HIP_COMPILER="${env:HIP_PATH}\lib\llvm\bin\clang.exe" `
-DHIP_PATH="${env:HIP_PATH}" `
-DGGML_HIP_ROCWMMA_FATTN=ON `
-DAMDGPU_TARGETS="${{ matrix.gpu_targets }}"
cmake --build . --config Release --parallel ${env:NUMBER_OF_PROCESSORS}
cmake --build build --config Release --parallel ${env:NUMBER_OF_PROCESSORS} --target ggml-hip
- name: Verify HIP backend was built
run: |
$hipDll = Get-ChildItem -Path build\bin -Filter "ggml-hip*.dll" -ErrorAction SilentlyContinue
$hipDll = Get-ChildItem -Path build\bin\Release -Filter "ggml-hip*.dll" -ErrorAction SilentlyContinue
if (-not $hipDll) {
Write-Host "##[error]ggml-hip*.dll was NOT produced. The HIP backend silently failed to build."
Write-Host "Contents of build\bin:"
Get-ChildItem build\bin | Format-Table -AutoSize
Write-Host "Contents of build\bin\Release:"
Get-ChildItem build\bin\Release | Format-Table -AutoSize
exit 1
}
Write-Host "HIP backend artifact found:"
@@ -836,10 +829,40 @@ jobs:
$rocmVersionShort = ('${{ matrix.ROCM_VERSION }}'.Split('.')[0..1] -join '.')
echo "ROCM_VERSION_SHORT=$rocmVersionShort" >> $env:GITHUB_ENV
- name: Bundle HIP runtime DLLs (amdhip64_7.dll, rocm_kpack.dll, amd_comgr.dll)
run: |
$ErrorActionPreference = "Stop"
# See issue https://github.com/ggml-org/llama.cpp/issues/26929.
# ggml-hip.dll loads amdhip64_7.dll at run time. The Adrenalin driver
# ships an amdhip64_7.dll in System32, which the loader searches before PATH,
# so a matching DLL from PATH cannot win. Copy amdhip64 next to the
# binaries (exe directory is searched before System32) so the correct
# runtime is used. rocm_kpack.dll is amdhip64_7's direct dependency, so
# copy the matching version too. amd_comgr is copied as well to keep it
# in sync with the bundled amdhip64, avoiding a version mismatch with a
# amd_comgr from System32.
# rocblas/hipblaslt kernels resolve fine via PATH and are not copied.
$binPath = (rocm-sdk path --bin).Trim()
if (-not $binPath) { throw "rocm-sdk path --bin returned empty" }
write-host "ROCm bin path: $binPath"
$patterns = @("amdhip64_7.dll", "rocm_kpack.dll", "amd_comgr.dll")
foreach ($pattern in $patterns) {
$files = Get-ChildItem -Path $binPath -Filter $pattern -ErrorAction SilentlyContinue
if (-not $files) { throw "no match for $pattern in $binPath" }
foreach ($f in $files) {
Copy-Item $f.FullName -Destination build\bin\Release -Force
write-host " copied $($f.Name)"
}
}
- name: Pack artifacts
run: |
cp "LICENSE" "build\bin\"
7z a -snl llama-bin-win-rocm-${{ env.ROCM_VERSION_SHORT }}-${{ matrix.build }}.zip .\build\bin\*
7z a -snl llama-bin-win-rocm-${{ env.ROCM_VERSION_SHORT }}-${{ matrix.build }}.zip `
.\build\bin\Release\ggml-hip.dll `
.\build\bin\Release\amdhip64_7.dll `
.\build\bin\Release\rocm_kpack.dll `
.\build\bin\Release\amd_comgr.dll
- name: Upload artifacts
uses: actions/upload-artifact@v6
+2 -2
View File
@@ -189,8 +189,8 @@ if [ ! -z ${GG_BUILD_OPENVINO} ]; then
fi
CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_OPENVINO=ON"
# TODO: fix and re-enable the `test-llama-archs` test below
CTEST_EXTRA="-E test-llama-archs|test-recurrent-state-rollback-nemotron-h"
# TODO: fix and re-enable the `test-llama-archs` and `test-recurrent-state-rollback*`
CTEST_EXTRA="-E test-llama-archs|^test-recurrent-state-rollback"
fi
## helpers
+53
View File
@@ -1643,6 +1643,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
}
}
).set_env("LLAMA_ARG_CTX_SIZE"));
add_opt(common_arg(
{ "--kv-unified-per-slot" }, "N",
"context limit per parallel slot (default: unset, behavior unchanged).\n"
"when set without -c/--ctx-size, the shared KV pool is sized to n_parallel*N",
[](common_params & params, int value) {
params.kv_unified_per_slot = value;
}
).set_env("LLAMA_ARG_KV_UNIFIED_PER_SLOT").set_examples({ LLAMA_EXAMPLE_SERVER }));
add_opt(common_arg(
{"-n", "--predict", "--n-predict"}, "N",
string_format(
@@ -2720,6 +2728,19 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
else { throw std::invalid_argument("invalid value"); }
}
).set_env("LLAMA_ARG_LOAD_MODE"));
add_opt(common_arg(
{"--tensor-read-lazy"}, "MODE",
"on-demand reading of certain tensors, for example per-layer embeddings (default: auto)\n"
"- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)\n"
"- auto: on, but only for tensors larger than 4 GiB\n"
"- off: always keep them resident",
[](common_params & params, const std::string & value) {
/**/ if (value == "on") { params.tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_ON; }
else if (value == "auto") { params.tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_AUTO; }
else if (value == "off") { params.tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_OFF; }
else { throw std::invalid_argument("invalid value"); }
}
).set_env("LLAMA_ARG_TENSOR_READ_LAZY"));
add_opt(common_arg(
{"--numa"}, "TYPE",
"attempt optimizations that help on some NUMA systems\n"
@@ -4132,6 +4153,38 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.speculative.draft.n_min = value;
}
).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_LOOKUP, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_DRAFT_N_MIN"));
add_opt(common_arg(
{"--spec-synth-len"}, "L",
"target mean synthetic acceptance length, including the target token (benchmarking only)",
[](common_params & params, const std::string & value) {
const std::string text = string_strip(value);
size_t pos = 0;
const double length = std::stod(text, &pos);
if (pos != text.size() || length == -1.0) {
throw std::invalid_argument("invalid value");
}
params.speculative.synth_len = length;
}
).set_spec().set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_SYNTH_LEN"));
add_opt(common_arg(
{"--spec-synth-rates"}, "P0,P1,...",
"comma-separated unconditional per-position synthetic acceptance probabilities (benchmarking only)",
[](common_params & params, const std::string & value) {
const auto values = string_split<std::string>(value, ',');
std::vector<double> rates;
rates.reserve(values.size());
for (const auto & raw : values) {
const std::string text = string_strip(raw);
size_t pos = 0;
const double rate = std::stod(text, &pos);
if (pos != text.size()) {
throw std::invalid_argument("invalid value");
}
rates.push_back(rate);
}
params.speculative.synth_rates = std::move(rates);
}
).set_spec().set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_SPEC_SYNTH_RATES"));
add_opt(common_arg(
{"--spec-draft-p-split", "--draft-p-split"}, "P",
+1
View File
@@ -1688,6 +1688,7 @@ struct llama_model_params common_model_params_to_llama(common_params & params) {
mparams.main_gpu = params.main_gpu;
mparams.split_mode = params.split_mode;
mparams.load_mode = params.load_mode;
mparams.tensor_read_lazy = params.tensor_read_lazy;
mparams.tensor_split = params.tensor_split;
mparams.check_tensors = params.check_tensors;
mparams.use_extra_bufts = !params.no_extra_bufts;
+10
View File
@@ -370,6 +370,9 @@ struct common_params_speculative_ngram_cache {
struct common_params_speculative {
std::vector<enum common_speculative_type> types = { COMMON_SPECULATIVE_TYPE_NONE };
double synth_len = -1.0;
std::vector<double> synth_rates;
// used by Simple, MTP, Eagle3, etc. - all methods that require some kind of draft model
common_params_speculative_draft draft;
@@ -384,6 +387,10 @@ struct common_params_speculative {
return !draft.mparams.empty();
}
bool has_synth() const {
return synth_len != -1.0 || !synth_rates.empty();
}
uint32_t need_n_rs_seq() const {
bool needs_rs_seq = std::any_of(types.begin(), types.end(), [&](auto t) {
return t == COMMON_SPECULATIVE_TYPE_DRAFT_MTP || t == COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3 || t == COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH || t == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK;
@@ -476,6 +483,8 @@ struct common_params {
enum llama_split_mode split_mode = LLAMA_SPLIT_MODE_LAYER; // how to split the model across GPUs
enum llama_load_mode load_mode = LLAMA_LOAD_MODE_AUTO; // how to load the model
enum llama_tensor_read_lazy tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_AUTO; // on-demand reading of tensors marked by the arch
common_cpu_params cpuparams;
common_cpu_params cpuparams_batch;
@@ -618,6 +627,7 @@ struct common_params {
bool cache_prompt = true; // whether to enable prompt caching
bool cache_idle_slots = true; // save and clear idle slots upon starting a new task
int32_t n_ctx_checkpoints = 32; // max number of context checkpoints per slot
int32_t kv_unified_per_slot = 0; // max context per parallel slot; 0 = unset
int32_t checkpoint_min_step = 8192; // minimum spacing between context checkpoints
int32_t cache_ram_mib = 8192; // -1 = no limit, 0 - disable, 1 = 1 MiB, etc.
+228 -21
View File
@@ -14,6 +14,7 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <map>
@@ -138,6 +139,7 @@ struct common_speculative_impl {
const common_speculative_type type;
uint32_t n_seq;
int32_t n_max; // maximum draft length after implementation-specific limits
size_t n_call_begin = 0; // number of times this implementation was called for refresh.
size_t n_call_draft = 0; // number of times this implementation was called for generation.
@@ -157,7 +159,7 @@ struct common_speculative_impl {
int64_t t_draft_us = 0; // total time spent in generating drafts in this implementation in microseconds.
int64_t t_accept_us = 0; // total time spent in accumulation of this implementation in microseconds.
common_speculative_impl(common_speculative_type type, uint32_t n_seq) : type(type), n_seq(n_seq) {}
common_speculative_impl(common_speculative_type type, uint32_t n_seq, int32_t n_max) : type(type), n_seq(n_seq), n_max(n_max) {}
virtual ~common_speculative_impl() = default;
@@ -182,7 +184,7 @@ struct common_speculative_impl_draft_simple : public common_speculative_impl {
std::vector<common_sampler_ptr> smpls;
common_speculative_impl_draft_simple(const common_params_speculative & params, uint32_t n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_SIMPLE, n_seq, params.draft.n_max)
, params(params.draft)
{
auto * ctx_dft = this->params.ctx_dft;
@@ -452,7 +454,7 @@ struct common_speculative_impl_draft_eagle3 : public common_speculative_impl {
std::vector<float> g_embd_buf;
common_speculative_impl_draft_eagle3(const common_params_speculative & params, uint32_t n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3, n_seq, params.draft.n_max)
, params(params.draft)
{
SPC_TRC("%s", "adding speculative implementation 'draft-eagle3'\n");
@@ -923,12 +925,19 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
int32_t block_size = 0;
llama_token mask_token_id = 0;
bool is_dflash2 = false;
bool is_mrope = false;
int32_t selector_top_k = 0;
// draft-dspark: the draft carries a Markov head and uses an anchor-first block layout
const bool is_dspark;
// dspark speculators
bool sample_from_anchor = true;
// block-internal attention
bool causal_attn = false;
const int32_t * target_layer_ids = nullptr; // model_dft's extract layer indices
uint32_t target_layer_ids_n = 0;
@@ -937,7 +946,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
common_speculative_impl_draft_dflash(const common_params_speculative & params, uint32_t n_seq,
common_speculative_type type = COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH)
: common_speculative_impl(type, n_seq)
: common_speculative_impl(type, n_seq, params.draft.n_max)
, params(params.draft)
, is_dspark(type == COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK)
{
@@ -966,9 +975,25 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
if (llama_model_meta_val_str(model_dft, "dflash.sample_from_anchor", buf, sizeof(buf)) >= 0) {
sample_from_anchor = std::strcmp(buf, "true") == 0;
}
if (llama_model_meta_val_str(model_dft, "dflash.attention.causal", buf, sizeof(buf)) >= 0) {
causal_attn = std::strcmp(buf, "true") == 0;
}
}
selector_top_k = llama_model_dflash_selector_top_k(model_dft);
is_dflash2 = selector_top_k > 0;
mask_token_id = llama_vocab_mask(llama_model_get_vocab(model_dft));
if (is_dspark && this->params.p_min > 0.0f) {
char buf[16] = {};
const bool has_conf =
llama_model_meta_val_str(model_dft, "dflash.has_confidence_head", buf, sizeof(buf)) < 0 ||
std::strcmp(buf, "true") == 0;
if (!has_conf) {
throw std::runtime_error("DSpark draft has no confidence head: please set --spec-draft-p-min 0");
}
}
LOG_INF("%s: adding speculative implementation '%s'\n", __func__, common_speculative_type_to_str(type).c_str());
LOG_INF("%s: - n_max=%d, n_min=%d, p_min=%.2f\n", __func__, this->params.n_max, this->params.n_min, this->params.p_min);
LOG_INF("%s: - block_size=%d, mask_token_id=%d, n_extract=%u, sample_from_anchor=%s\n", __func__,
@@ -983,10 +1008,18 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
this->params.n_max = std::min(this->params.n_max, n_draft_max);
this->params.n_min = std::min(this->params.n_min, n_draft_max);
}
this->n_max = this->params.n_max;
batch = llama_batch_init(llama_n_batch(ctx_dft), 0, n_seq);
batch_inject = llama_batch_init(llama_n_batch(ctx_dft), n_embd_dec, n_seq);
// embd batches on an M-RoPE draft need 4 position rows per token
is_mrope = llama_model_rope_type(model_dft) == LLAMA_ROPE_TYPE_MROPE;
if (is_mrope) {
free(batch_inject.pos);
batch_inject.pos = (llama_pos *) malloc(sizeof(llama_pos) * 4 * llama_n_batch(ctx_dft));
}
smpls.resize(n_seq);
for (auto & s : smpls) {
common_params_sampling sparams;
@@ -998,7 +1031,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
// offload draft sampling to the backend
backend_chains.assign(n_seq, nullptr);
if (this->params.backend_sampling) {
if (this->params.backend_sampling && !is_dflash2) {
for (llama_seq_id seq_id = 0; seq_id < (llama_seq_id) n_seq; ++seq_id) {
llama_sampler * chain = llama_sampler_chain_init(llama_sampler_chain_default_params());
llama_sampler_chain_add(chain, llama_sampler_init_top_k(10));
@@ -1017,8 +1050,9 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
llama_set_embeddings_layer_inp(ctx_tgt, (uint32_t) target_layer_ids[k], true);
}
llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ true);
llama_set_causal_attn(ctx_dft, false); // DFlash needs non-causal attention
// DFlash2 reads its selector lattice from h_nextn and never consumes raw logits.
llama_set_embeddings_nextn(ctx_dft, true, /*masked*/ !is_dflash2);
llama_set_causal_attn(ctx_dft, causal_attn); // DFlash needs non-causal attention unless the model says otherwise
}
~common_speculative_impl_draft_dflash() override {
@@ -1118,11 +1152,24 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
}
// fuse extracted features through DFlash encoder
// M-RoPE drafts read 4 position rows per token from embd batches, so pass them explicitly
std::vector<llama_pos> enc_pos;
if (is_mrope) {
enc_pos.resize((size_t) 4 * n_chunk);
for (int32_t i = 0; i < n_chunk; ++i) {
const llama_pos p = batch_in.pos[i_batch_beg[seq_id] + offset + i];
enc_pos[0 * n_chunk + i] = p;
enc_pos[1 * n_chunk + i] = p;
enc_pos[2 * n_chunk + i] = p;
enc_pos[3 * n_chunk + i] = 0;
}
}
llama_batch enc_batch = {
/*.n_tokens =*/ n_chunk,
/*.token =*/ nullptr,
/*.embd =*/ features_buf.data(),
/*.pos =*/ nullptr,
/*.pos =*/ is_mrope ? enc_pos.data() : nullptr,
/*.n_seq_id =*/ nullptr,
/*.seq_id =*/ nullptr,
/*.logits =*/ nullptr,
@@ -1143,7 +1190,13 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
std::memcpy(batch_inject.embd, inp_g, (size_t) n_chunk * n_embd_dec * sizeof(float));
for (int32_t i = 0; i < n_chunk; ++i) {
batch_inject.pos[i] = batch_in.pos[i_batch_beg[seq_id] + offset + i];
const llama_pos p = batch_in.pos[i_batch_beg[seq_id] + offset + i];
batch_inject.pos[i] = p;
if (is_mrope) {
batch_inject.pos[1 * n_chunk + i] = p;
batch_inject.pos[2 * n_chunk + i] = p;
batch_inject.pos[3 * n_chunk + i] = 0;
}
batch_inject.n_seq_id[i] = 1;
batch_inject.seq_id[i][0] = seq_id;
batch_inject.logits[i] = false;
@@ -1186,7 +1239,7 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
i_block_beg[seq_id] = batch.n_tokens;
n_block [seq_id] = n_block_tokens;
for (int32_t i = 0; i < n_block_tokens; ++i) {
common_batch_add(batch, i == 0 ? dp.id_last : mask_token_id, n + i, { seq_id }, true);
common_batch_add(batch, i == 0 ? dp.id_last : mask_token_id, n + i, { seq_id }, !is_dflash2);
}
}
@@ -1214,6 +1267,36 @@ struct common_speculative_impl_draft_dflash : public common_speculative_impl {
auto & result = *dp.result;
if (is_dflash2) {
const float * lattice = llama_get_embeddings_nextn(ctx_dft);
GGML_ASSERT(lattice && "DFlash2 selector produced no lattice");
int32_t predecessor = 0;
for (int32_t i = 1; i < n_block_tokens; ++i) {
const float * row = lattice + (size_t) (beg + i) * n_embd_dec;
const float * scores = row + selector_top_k + (size_t) predecessor * selector_top_k;
predecessor = (int32_t) std::distance(scores,
std::max_element(scores, scores + selector_top_k));
if (params.p_min > 0.0f) {
// softmax(scores) at the argmax, i.e. 1 / sum(exp(s_k - s_max))
float sum = 0.0f;
for (int32_t k = 0; k < selector_top_k; ++k) {
sum += std::exp(scores[k] - scores[predecessor]);
}
if (1.0f / sum < params.p_min) {
break;
}
}
result.push_back((llama_token) row[predecessor]);
}
if (result.size() < (size_t) params.n_min) {
result.clear();
}
continue;
}
if (is_dspark) {
// DSpark: read from the first draft slot, truncate below the confidence threshold
const float * conf = params.p_min > 0.0f ? llama_get_embeddings_nextn(ctx_dft) : nullptr;
@@ -1315,7 +1398,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
std::vector<std::vector<float>> chain_h;
common_speculative_impl_draft_mtp(const common_params_speculative & params, uint32_t n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, n_seq, params.draft.n_max)
, params(params.draft)
{
auto * ctx_tgt = this->params.ctx_tgt;
@@ -1382,6 +1465,7 @@ struct common_speculative_impl_draft_mtp : public common_speculative_impl {
c.reserve((size_t) (this->params.n_max + 1) * n_embd);
}
}
this->n_max = this->params.n_max;
pending_h.assign(n_seq, std::vector<float>(n_embd, 0.0f));
@@ -1726,7 +1810,7 @@ struct common_speculative_impl_ngram_simple : public common_speculative_impl {
common_speculative_impl_ngram_simple(
const common_params_speculative & params, uint32_t n_seq,
common_ngram_simple_config config)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_SIMPLE, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_SIMPLE, n_seq, params.ngram_simple.size_m)
, params(params.ngram_simple)
, config(config)
{
@@ -1770,7 +1854,7 @@ struct common_speculative_impl_ngram_map_k : public common_speculative_impl {
const common_ngram_map & config,
uint32_t n_seq)
: common_speculative_impl(config.key_only ? COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K
: COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K4V, n_seq)
: COMMON_SPECULATIVE_TYPE_NGRAM_MAP_K4V, n_seq, config.size_value)
{
for (uint32_t i = 0; i < n_seq; i++) {
this->config.push_back(config);
@@ -1841,7 +1925,7 @@ struct common_speculative_impl_ngram_mod : public common_speculative_impl {
common_speculative_impl_ngram_mod(
const common_params_speculative & params,
uint32_t n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_MOD, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_MOD, n_seq, params.ngram_mod.n_max)
, params(params.ngram_mod)
, mod(params.ngram_mod.n_match, 4*1024*1024)
, verbose(std::getenv("LLAMA_TRACE") != nullptr) {
@@ -2017,7 +2101,7 @@ struct common_speculative_impl_ngram_cache : public common_speculative_impl {
const std::string & path_dynamic,
bool save_dynamic,
bool save_static)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_CACHE, n_seq)
: common_speculative_impl(COMMON_SPECULATIVE_TYPE_NGRAM_CACHE, n_seq, n_draft)
, params(params.ngram_cache)
, n_draft(n_draft)
, save_dynamic(save_dynamic)
@@ -2138,6 +2222,8 @@ struct common_speculative {
// which implementaion was used for a given seq_id
std::vector<common_speculative_impl *> impl_last;
std::vector<double> synth_probs;
};
static common_ngram_map get_common_ngram_map(
@@ -2316,6 +2402,101 @@ int32_t common_speculative_n_max(const common_params_speculative * spec) {
return n_max;
}
int32_t common_speculative_n_max(const common_speculative * spec) {
int32_t n_max = 0;
if (spec == nullptr) {
return n_max;
}
for (const auto & impl : spec->impls) {
n_max = std::max(n_max, std::max(0, impl->n_max));
}
return n_max;
}
std::vector<double> common_speculative_synth_rates_resolve(const common_params_speculative * spec, int32_t n_max) {
const bool has_length = spec->synth_len != -1.0;
const bool has_rates = !spec->synth_rates.empty();
if (!has_length && !has_rates) {
return {};
}
if (has_length && has_rates) {
throw std::invalid_argument("synthetic acceptance length and rates are mutually exclusive");
}
if (n_max <= 0) {
throw std::invalid_argument("synthetic acceptance requires at least one speculative token");
}
if (has_rates) {
const auto & rates = spec->synth_rates;
if (rates.size() != (size_t) n_max) {
throw std::invalid_argument(string_format(
"synthetic acceptance rates must contain %d values, got %zu", n_max, rates.size()));
}
for (size_t i = 0; i < rates.size(); ++i) {
if (!std::isfinite(rates[i]) || rates[i] < 0.0 || rates[i] > 1.0) {
throw std::invalid_argument("synthetic acceptance rates must be finite and within [0, 1]");
}
if (i > 0 && rates[i] > rates[i - 1]) {
throw std::invalid_argument("synthetic acceptance rates must be monotonically non-increasing");
}
}
return rates;
}
const double length = spec->synth_len;
const double length_max = (double) n_max + 1.0;
if (!std::isfinite(length) || length < 1.0 || length > length_max) {
throw std::invalid_argument(string_format(
"synthetic acceptance length must be finite and within [1, %.0f]", length_max));
}
double p = 0.0;
if (length == length_max) {
p = 1.0;
} else if (length > 1.0) {
double p_min = 0.0;
double p_max = 1.0;
for (int i = 0; i < 32; ++i) {
const double p_mid = 0.5 * (p_min + p_max);
double sum = 0.0;
double term = p_mid;
for (int32_t j = 0; j < n_max; ++j) {
sum += term;
term *= p_mid;
}
if (sum < length - 1.0) {
p_min = p_mid;
} else {
p_max = p_mid;
}
}
p = 0.5 * (p_min + p_max);
}
std::vector<double> rates;
rates.reserve(n_max);
double rate = p;
for (int32_t i = 0; i < n_max; ++i) {
rates.push_back(rate);
rate *= p;
}
return rates;
}
const std::vector<double> & common_speculative_get_synth_probs(const common_speculative * spec) {
GGML_ASSERT(spec);
return spec->synth_probs;
}
common_params common_base_params_to_speculative(const common_params & params) {
const bool has_draft = params.speculative.has_dft();
@@ -2568,13 +2749,39 @@ common_speculative * common_speculative_init(common_params_speculative & params,
return nullptr;
}
auto * result = new common_speculative {
/* .dparams = */ common_speculative_draft_params_vec(n_seq),
/* .impls = */ std::move(impls),
/* .impl_last = */ std::vector<common_speculative_impl *>(n_seq, nullptr)
};
common_speculative_ptr result(new common_speculative {
/* .dparams = */ common_speculative_draft_params_vec(n_seq),
/* .impls = */ std::move(impls),
/* .impl_last = */ std::vector<common_speculative_impl *>(n_seq, nullptr),
/* .synth_probs = */ {},
});
return result;
const int32_t n_max_configured = common_speculative_n_max(&params);
const int32_t n_max_effective = common_speculative_n_max(result.get());
const auto rates = common_speculative_synth_rates_resolve(&params, n_max_effective);
std::vector<std::string> rates_str;
rates_str.reserve(rates.size());
result->synth_probs.reserve(rates.size());
double rate_prev = 1.0;
double acceptance_length = 1.0;
for (const double rate : rates) {
result->synth_probs.push_back(rate_prev > 0.0 ? rate / rate_prev : 0.0);
rates_str.push_back(string_format("%.6g", rate));
rate_prev = rate;
acceptance_length += rate;
}
if (!result->synth_probs.empty()) {
SPC_WRN("%s", "synthetic speculative acceptance is enabled for benchmarking; generated output is not valid\n");
if (n_max_effective != n_max_configured) {
SPC_WRN("synthetic acceptance draft limit was reduced from %d to %d by the initialized speculative implementations\n",
n_max_configured, n_max_effective);
}
SPC_INF("synthetic acceptance: n_max = %zu, mean length = %.6f, rates = [%s]\n",
rates.size(), acceptance_length, string_join(rates_str, ", ").c_str());
}
return result.release();
}
void common_speculative_free(common_speculative * spec) {
+9
View File
@@ -26,6 +26,15 @@ std::string common_speculative_type_to_str(enum common_speculative_type type);
// return the max number of draft tokens based on the speculative parameters
int32_t common_speculative_n_max(const common_params_speculative * spec);
// return the max number of draft tokens from the initialized implementations
int32_t common_speculative_n_max(const common_speculative * spec);
// validate and resolve the unconditional synthetic acceptance rates
std::vector<double> common_speculative_synth_rates_resolve(const common_params_speculative * spec, int32_t n_max);
// return the conditional synthetic acceptance probabilities
const std::vector<double> & common_speculative_get_synth_probs(const common_speculative * spec);
common_params common_base_params_to_speculative(const common_params & params);
struct common_speculative_output_limits {
+4
View File
@@ -54,6 +54,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"DeepseekV3ForCausalLM": "deepseek",
"DeepseekV32ForCausalLM": "deepseek",
"DFlashDraftModel": "qwen",
"DFlash2DraftModel": "qwen",
"Qwen3DSparkModel": "qwen",
"DSparkDraftModel": "qwen",
"DSparkSpeculator": "qwen",
@@ -235,6 +236,8 @@ TEXT_MODEL_MAP: dict[str, str] = {
"Qwen3_5ForConditionalGeneration": "qwen",
"Qwen3_5MoeForCausalLM": "qwen",
"Qwen3_5MoeForConditionalGeneration": "qwen",
"Qwen4ExpForCausalLM": "qwen4exp",
"Qwen4ExpForConditionalGeneration": "qwen4exp",
"RND1": "qwen",
"RWForCausalLM": "falcon",
"RWKV6Qwen2ForCausalLM": "rwkv",
@@ -332,6 +335,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
"Qwen3VLMoeForConditionalGeneration": "qwen3vl",
"Qwen3_5ForConditionalGeneration": "qwen3vl",
"Qwen3_5MoeForConditionalGeneration": "qwen3vl",
"Qwen4ExpForConditionalGeneration": "qwen4exp",
"RADIOModel": "nemotron",
"Sarashina2VisionForCausalLM": "sarashina2",
"SmolVLMForConditionalGeneration": "smolvlm",
+6 -2
View File
@@ -1006,12 +1006,16 @@ class ModelBase:
else:
raise ValueError(f"Unknown file type: {self.ftype.name}")
# a chunked tensor quantizes as one chunk at a time, while it is written
quantize = data.quantize if isinstance(data, gguf.LazyChunkedTensor) else (
lambda qtype, d=data: gguf.quants.quantize(d, qtype))
try:
data = gguf.quants.quantize(data, data_qtype)
data = quantize(data_qtype)
except gguf.QuantError as e:
logger.warning("%s, %s", e, "falling back to F16")
data_qtype = gguf.GGMLQuantizationType.F16
data = gguf.quants.quantize(data, data_qtype)
data = quantize(data_qtype)
shape = gguf.quant_shape_from_byte_shape(data.shape, data_qtype) if data.dtype == np.uint8 else data.shape
+4
View File
@@ -302,6 +302,10 @@ class NemotronHModel(GraniteHybridModel):
)
if not keep:
return None
# PEFT names adapter tensors using model.layers.*, while Nemotron-H checkpoints
# and the GGUF tensor map use backbone.layers.*
if name.startswith("model.layers.") and ".mixer." in name:
name = name.replace("model.layers.", "backbone.layers.", 1)
return super().filter_tensors((name, gen))
def prepare_metadata(self, vocab_only: bool):
+74 -6
View File
@@ -639,7 +639,7 @@ class Qwen3_5MoeTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
model_arch = gguf.MODEL_ARCH.QWEN35MOE
@ModelBase.register("DFlashDraftModel")
@ModelBase.register("DFlashDraftModel", "DFlash2DraftModel")
@ModelBase.example("z-lab/Qwen3.5-9B-DFlash")
class DFlashModel(Qwen3Model):
model_arch = gguf.MODEL_ARCH.DFLASH
@@ -678,34 +678,98 @@ class DFlashModel(Qwen3Model):
def set_gguf_parameters(self):
super().set_gguf_parameters()
block_size = self.hparams.get("block_size", 16)
self.gguf_writer.add_block_size(block_size)
dflash_config = self.hparams.get("dflash_config", {})
block_size = dflash_config.get("block_size", self.hparams.get("block_size", 16))
self.gguf_writer.add_block_size(block_size)
if "conv_kernel_size" in dflash_config:
self.gguf_writer.add_conv_kernel_size(int(dflash_config["conv_kernel_size"]))
self.gguf_writer.add_conv_group_size(int(dflash_config["conv_group_size"]))
self.gguf_writer.add_selector_rank(int(dflash_config["selector_rank"]))
self.gguf_writer.add_selector_top_k(int(dflash_config["selector_top_k"]))
output_multiplier = dflash_config.get(
"output_multiplier", self.hparams.get("output_multiplier")
)
if output_multiplier is not None:
self.gguf_writer.add_logit_scale(float(output_multiplier))
softcap = dflash_config.get(
"final_logit_softcapping", self.hparams.get("final_logit_softcapping")
)
if softcap is not None and float(softcap) > 0:
self.gguf_writer.add_final_logit_softcapping(float(softcap))
embedding_scale = dflash_config.get(
"input_embedding_scale", self.hparams.get("input_embedding_scale")
)
if embedding_scale is not None:
self.gguf_writer.add_embedding_scale(float(embedding_scale))
target_layer_ids = dflash_config.get("target_layer_ids", [])
if target_layer_ids:
extract_layer_ids = [i + 1 for i in target_layer_ids]
self.gguf_writer.add_target_layers(extract_layer_ids)
use_sliding_window = self.hparams.get("use_sliding_window", False)
sliding_window = self.hparams.get("sliding_window")
use_sliding_window = self.hparams.get("use_sliding_window", False) or dflash_config.get("use_swa", False)
sliding_window = dflash_config.get("swa_window_size") or self.hparams.get("sliding_window")
layer_types = self.hparams.get("layer_types")
if use_sliding_window and sliding_window and layer_types:
is_swa = [lt == "sliding_attention" for lt in layer_types]
self.gguf_writer.add_sliding_window(sliding_window)
self.gguf_writer.add_sliding_window_pattern(is_swa)
causal = self.hparams.get("is_causal")
if causal is None:
causal = dflash_config.get("causal")
if causal is not None:
self.gguf_writer.add_causal_attention(bool(causal))
# M-RoPE target: the draft ropes on the temporal dim only, so write
# degenerate sections [n_rot/2, 0, 0, 0]
if self._target_uses_mrope():
head_dim = self.hparams.get("head_dim") or self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
self.gguf_writer.add_rope_dimension_sections([head_dim // 2, 0, 0, 0])
def _target_uses_mrope(self) -> bool:
if self.target_model_dir is None:
return False
with open(self.target_model_dir / "config.json", "r", encoding="utf-8") as f:
cfg = json.load(f)
cfg = cfg.get("text_config", cfg)
rope = cfg.get("rope_parameters") or cfg.get("rope_scaling") or {}
return "mrope_section" in rope
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
name, gen = item
if not name.startswith("model."):
name = "model." + name
if "sink" in name and not name.endswith(".weight"):
name += ".weight"
return super().filter_tensors((name, gen))
_ROPE_PERMUTE_SUFFIXES = (
"self_attn.q_proj.weight",
"self_attn.k_proj.weight",
"self_attn.q_norm.weight",
"self_attn.k_norm.weight",
)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
if name == "model.embed_tokens.weight" and not self.hparams.get("has_embed_tokens", True):
return
# interleaved-rope checkpoints (rope_is_neox_style = false) -> NeoX layout: per head, even dims first then odd
if not self.hparams.get("rope_is_neox_style", True) and name.endswith(self._ROPE_PERMUTE_SUFFIXES):
head_dim = self.hparams["head_dim"]
shape = data_torch.shape
data_torch = data_torch.reshape(-1, head_dim // 2, 2, *shape[1:]).transpose(1, 2).reshape(shape)
if name in (
"model.candidate_selector.predecessor_codebook",
"model.candidate_selector.successor_codebook",
):
name += ".weight"
yield from super().modify_tensors(data_torch, name, bid)
@@ -759,6 +823,10 @@ class DSparkModel(DFlashModel):
super().set_gguf_parameters()
self.gguf_writer.add_sample_from_anchor(self._sample_from_anchor)
# confidence head is optional: vanilla-markov exports ship without it
has_conf = any("confidence_head.proj" in name for name in self.model_tensors)
self.gguf_writer.add_has_confidence_head(has_conf)
@classmethod
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
if item[0] == "t2d": # not used at runtime
@@ -777,7 +845,7 @@ class DSparkModel(DFlashModel):
self._d2t = data_torch
return
if self._n_vocab_draft == self.hparams["vocab_size"] and name.endswith(("embed_tokens.weight", "lm_head.weight")):
if self._n_vocab_draft == self.hparams["vocab_size"] and name.endswith("lm_head.weight"):
return
# interleaved-rope checkpoints (rope_is_neox_style = false) -> NeoX layout: per head, even dims first then odd
+195
View File
@@ -0,0 +1,195 @@
from __future__ import annotations
from typing import Iterable, cast
import torch
from torch import Tensor
import gguf
import numpy as np
from .base import ModelBase
from .qwen import _LinearAttentionVReorderBase, _Qwen35MRopeMixin
from .qwen3vl import Qwen3VLVisionModel
@ModelBase.register("Qwen4ExpForConditionalGeneration", "Qwen4ExpForCausalLM")
@ModelBase.example("Qwen/Qwen3.8-Flash-Next")
class Qwen4ExpTextModel(_Qwen35MRopeMixin, _LinearAttentionVReorderBase):
"""Qwen3.8-Flash-Next.
Shares the Qwen3.5 gated delta net and interleaved mrope, and adds three things:
hyper-connections in place of every layer norm, QSA sparse attention on the full
attention layers, and PLE n-gram hash embeddings on a single layer.
"""
model_arch = gguf.MODEL_ARCH.QWEN4EXP
# the MTP block is a separate draft head; vLLM drops it too
supports_mtp_export = False
no_mtp = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# only the shard names, so the table itself is never held
self._ple_shards: dict[int, str] = {}
self._ple_row_dim: int | None = None
def _read_hash_constants(self, suffix: str) -> list[int]:
"""Read an int64 PLE constant straight from the checkpoint.
prepare_tensors() casts every non-float dtype to float32 before
modify_tensors() sees it (base.py), which would silently round these
45-bit multipliers. Reading the lazy tensor here bypasses that.
"""
for name, gen in self.model_tensors.items():
if name.endswith(suffix):
t = gen()
if t.dtype != torch.int64:
t = t.to(torch.int64)
return [int(x) for x in t.tolist()]
raise ValueError(f"PLE constant {suffix!r} missing from the checkpoint")
def set_gguf_parameters(self):
super().set_gguf_parameters()
hp = self.hparams
self.gguf_writer.add_hyper_connection_count(hp["hc_count"])
self.gguf_writer.add_hyper_connection_low_rank(hp["hc_lowrank"])
n_layer = hp["num_hidden_layers"]
self.gguf_writer.add_indexer_head_count(hp["indexer_n_heads"])
self.gguf_writer.add_indexer_key_length(hp["indexer_head_dim"])
self.gguf_writer.add_indexer_top_k(hp["indexer_budget"])
ratio = hp["indexer_compress_ratio"]
layer_types = hp["layer_types"]
self.gguf_writer.add_attention_compress_ratios(
[ratio if layer_types[i] == "full_attention" else 0 for i in range(n_layer)]
)
# ple_layer_ids is 1-based in the HF config; empty means no n-gram table,
# so emit no PLE keys rather than optional ones
ple_layers = [i - 1 for i in hp["ple_layer_ids"]]
if not ple_layers:
return
self.gguf_writer.add_ple_layers(ple_layers)
self.gguf_writer.add_ple_ngram_size(hp["ngram_size"])
self.gguf_writer.add_ple_heads_per_ngram(hp["heads_per_ngram"])
self.gguf_writer.add_ple_conv_kernel(hp["ple_conv_kernel_size"])
self.gguf_writer.add_ple_eos_token_id(self._eos_token_id())
# an image is decoded as an embeddings-only batch, so the graph has no placeholder
# ids to hash; carry the id and let it stand in for those positions
_img = self._image_token_id()
if _img is not None:
self.gguf_writer.add_ple_image_token_id(int(_img))
if self._ple_row_dim is not None:
self.gguf_writer.add_embedding_length_per_layer_input(self._ple_row_dim)
self.gguf_writer.add_ple_layer_multipliers(
self._read_hash_constants("ple_embedding.layer_multipliers"))
self.gguf_writer.add_ple_head_offsets(
self._read_hash_constants("ple_embedding.ngram_heads_offsets"))
self.gguf_writer.add_ple_head_vocab_sizes(
self._read_hash_constants("ple_embedding.ngram_heads_vocab_sizes"))
def _image_token_id(self) -> int | None:
img = self.hparams.get("image_token_id")
return None if img is None else int(img)
def _eos_token_id(self) -> int:
eos = self.hparams.get("eos_token_id")
if isinstance(eos, list):
# the PLE hash resets n-grams on the primary EOS
return int(eos[-1])
if eos is None:
raise ValueError("eos_token_id is required: the PLE hash resets its n-grams on it")
return int(eos)
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
# int64 hash constants must stay exact; 1-D tensors force F32, so use KV
if name.endswith("ple_embedding.layer_multipliers"):
self._ple_multipliers = [int(x) for x in data_torch.tolist()]
return []
if name.endswith("ple_embedding.ngram_heads_offsets"):
self._ple_head_offsets = [int(x) for x in data_torch.tolist()]
return []
if name.endswith("ple_embedding.ngram_heads_vocab_sizes"):
self._ple_head_vocab_sizes = [int(x) for x in data_torch.tolist()]
return []
if ".ngram_embedding.shard_" in name:
return self._place_ple_shard(data_torch, name)
# one projection feeds indexer q and k; split it, as minimax-m3 does
if ".indexer.index_qk_proj.weight" in name:
n_q = self.hparams["indexer_n_heads"] * self.hparams["indexer_head_dim"]
q = data_torch[:n_q]
k = data_torch[n_q:]
return [
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_Q_PROJ, bid, ".weight"), q),
(self.format_tensor_name(gguf.MODEL_TENSOR.INDEXER_K_PROJ, bid, ".weight"), k),
]
# Gemma zero-centred gammas the inherited norm.weight rule misses
if name.endswith((".ple.norm_key.weight", ".ple.norm_query.weight", ".ple.norm_conv.weight",
".indexer.q_layernorm.weight", ".indexer.k_layernorm.weight")):
return [(self.map_tensor_name(name), data_torch + 1)]
if name.endswith(".ple.conv1d.weight"):
return [(self.map_tensor_name(name), data_torch.squeeze())]
return super().modify_tensors(data_torch, name, bid)
# the shards concatenate into a tensor of well over 100 GB
# use LazyChunkedTensor here, a single shard resident at a time
def _place_ple_shard(self, data_torch: Tensor, name: str) -> Iterable[tuple[str, Tensor]]:
idx = int(name.rpartition(".shard_")[2].partition(".")[0])
n_parts = self.hparams["split_ngram_parts"]
self._ple_shards[idx] = name
self._ple_row_dim = int(data_torch.shape[-1])
if len(self._ple_shards) < n_parts:
return []
# the checkpoint may yield the shards in any order, the row order is by index
shards = [self._ple_shards[i] for i in sorted(self._ple_shards)]
rows = 0
for shard in shards:
shape = self.model_tensors[shard]().shape
if int(shape[-1]) != self._ple_row_dim:
raise ValueError(
f"PLE shard {shard} has row dim {int(shape[-1])}, expected {self._ple_row_dim}")
rows += int(shape[0])
table = gguf.LazyChunkedTensor(
[self._load_ple_shard(shard) for shard in shards],
shape=(rows, self._ple_row_dim),
dtype=np.float32,
)
gguf_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.PER_LAYER_TOKEN_EMBD]
return [(gguf_name + ".weight", cast(Tensor, table))]
def _load_ple_shard(self, name: str):
def load() -> np.ndarray:
from .base import LazyTorchTensor
# a fresh lazy tensor every call, or to_eager() memoizes every shard
eager = LazyTorchTensor.to_eager(self.model_tensors[name]())
return eager.to(torch.float32).contiguous().numpy()
return load
def prepare_tensors(self):
super().prepare_tensors()
n_parts = self.hparams.get("split_ngram_parts", 0)
if self._ple_shards and len(self._ple_shards) != n_parts:
raise ValueError(
f"got {len(self._ple_shards)} PLE embedding shards, expected {n_parts}"
)
@ModelBase.register("Qwen4ExpForConditionalGeneration")
@ModelBase.example("Qwen/Qwen3.8-Flash-Next")
class Qwen4ExpVisionModel(Qwen3VLVisionModel):
"""The vision tower is an unmodified Qwen3-VL ViT."""
+46 -40
View File
@@ -22,8 +22,8 @@ The OpenVINO backend is implemented in `ggml/src/ggml-openvino` and provides a t
- [0. Prerequisites](#0-prerequisites)
- [1. Install OpenVINO Runtime](#1-install-openvino-runtime)
- [2. Build llama.cpp with OpenVINO Backend](#2-build-llamacpp-with-openvino-backend)
- [Automated Ubuntu Build Script](#automated-ubuntu-build-script)
- [Automated Windows Build Script](#automated-windows-build-script)
- [Ubuntu Build Script](#ubuntu-build-script)
- [Windows Build Script](#windows-build-script)
- [3. Download Sample Model](#3-download-sample-model)
- [4. Run Inference with OpenVINO Backend](#4-run-inference-with-openvino-backend)
- [5. Docker Build](#5-docker-build)
@@ -96,7 +96,7 @@ Although, the validated models below were tested with `llama-cli` using the `Q4_
- **SL** = Stateless (`GGML_OPENVINO_STATEFUL_EXECUTION=0`)
- **SF** = Stateful (`GGML_OPENVINO_STATEFUL_EXECUTION=1`)
- Note: The NPU operates in stateless mode only.
- **Validation system:** Intel® Core™ Ultra 5 238V (Lunar Lake) | 32 GB RAM | Ubuntu 24.04 | Intel OpenCL GPU Driver 26.18.38308.1 | Intel NPU Driver 1.33.0.
- **Validation system:** Intel® Core™ Ultra 5 238V (Lunar Lake) | 32 GB RAM | Ubuntu 24.04 | Intel OpenCL GPU Driver 26.31.39395.13-0 | Intel NPU Driver 1.35.0.
- See [Known Limitations](#known-limitations) for context on observed failures.
| Model | CPU (SL / SF) | GPU (SL / SF) | NPU (SL) |
@@ -105,27 +105,32 @@ Although, the validated models below were tested with `llama-cli` using the `Q4_
| [bartowski/Llama-3.2-3B-Instruct-Q4_K_M](https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| [bartowski/Meta-Llama-3.1-8B-Instruct-Q4_K_M](https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| | | | |
| [Qwen/qwen2.5-1.5b-instruct-q4_k_m](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [Qwen/qwen2.5-coder-7b-instruct-q4_k_m](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Qwen_Qwen3-0.6B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3-0.6B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Qwen_Qwen3-1.7B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3-1.7B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [Qwen/Qwen3-4B-Q4_K_M](https://huggingface.co/Qwen/Qwen3-4B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [lm-kit/Qwen3-8B-Q4_K_M](https://huggingface.co/lm-kit/qwen-3-8b-instruct-gguf) | ✓ / ✓ | ✓ / | ✓ |
| [Qwen/qwen2.5-1.5b-instruct-q4_k_m](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [Qwen/qwen2.5-coder-7b-instruct-q4_k_m](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Qwen_Qwen3-0.6B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3-0.6B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Qwen_Qwen3-1.7B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3-1.7B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [Qwen/Qwen3-4B-Q4_K_M](https://huggingface.co/Qwen/Qwen3-4B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [lm-kit/Qwen3-8B-Q4_K_M](https://huggingface.co/lm-kit/qwen-3-8b-instruct-gguf) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Qwen_Qwen3.5-0.8B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3.5-0.8B-GGUF) | ✓ / ✗ | ✓ / ✗ | ✗ |
| [bartowski/Qwen_Qwen3.5-2B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3.5-2B-GGUF) | ✓ / ✗ | ✓ / ✗ | ✗ |
| [bartowski/Qwen_Qwen3.5-4B-Q4_K_M](https://huggingface.co/bartowski/Qwen_Qwen3.5-4B-GGUF) | ✓ / ✗ | ✓ / ✗ | ✗ |
| [lmstudio-community/Qwen3.5-9B-Q4_K_M](https://huggingface.co/lmstudio-community/Qwen3.5-9B-GGUF) | ✓ / ✗ | ✓ / ✗ | ✗ |
| | | | |
| [unsloth/gemma-3-4b-it-Q4_K_M](https://huggingface.co/unsloth/gemma-3-4b-it-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/google_gemma-4-E2B-it-Q4_K_M](https://huggingface.co/bartowski/google_gemma-4-E2B-it-GGUF) | ✓ / ✗ | ✓ / ✗ | |
| [unsloth/gemma-3-4b-it-Q4_K_M](https://huggingface.co/unsloth/gemma-3-4b-it-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/google_gemma-4-E2B-it-Q4_K_M](https://huggingface.co/bartowski/google_gemma-4-E2B-it-GGUF) | ✓ / ✗ | ✓ / ✗ | |
| [bartowski/google_gemma-4-E4B-it-Q4_K_M](https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF) | ✓ / ✗ | ✓ / ✗ | ✓ |
| [bartowski/gemma-4-12B-it-Q4_K_M](https://huggingface.co/bartowski/gemma-4-12B-it-GGUF) | ✓ / ✗ | ✓ / ✗ | |
| [bartowski/gemma-4-12B-it-Q4_K_M](https://huggingface.co/bartowski/gemma-4-12B-it-GGUF) | ✓ / ✗ | ✓ / ✗ | |
| | | | |
| [bartowski/Phi-3-mini-4k-instruct-Q4_K_M](https://huggingface.co/bartowski/Phi-3-mini-4k-instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Phi-3.5-mini-instruct-Q4_K_M](https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Phi-3-mini-4k-instruct-Q4_K_M](https://huggingface.co/bartowski/Phi-3-mini-4k-instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/Phi-3.5-mini-instruct-Q4_K_M](https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/microsoft_Phi-4-mini-instruct-Q4_K_M](https://huggingface.co/bartowski/microsoft_Phi-4-mini-instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| | | | |
| [bartowski/Mistral-7B-Instruct-v0.3-Q4_K_M](https://huggingface.co/bartowski/Mistral-7B-Instruct-v0.3-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| [QuantFactory/Ministral-3b-instruct.Q4_K_M](https://huggingface.co/QuantFactory/Ministral-3b-instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| [bartowski/Ministral-8B-Instruct-2410-Q4_K_M](https://huggingface.co/bartowski/Ministral-8B-Instruct-2410-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| | | | |
| [bartowski/DeepSeek-R1-Distill-Llama-8B-Q4_K_M](https://huggingface.co/bartowski/DeepSeek-R1-Distill-Llama-8B-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| [bartowski/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M](https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M](https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| | | | |
| [ibm-granite/granite-4.0-350m-Q4_K_M](https://huggingface.co/ibm-granite/granite-4.0-350m-GGUF) | ✓ / ✓ | ✗ / ✗ | ✓ |
| [ibm-granite/granite-4.0-micro-Q4_K_M](https://huggingface.co/ibm-granite/granite-4.0-micro-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
@@ -133,10 +138,10 @@ Although, the validated models below were tested with `llama-cli` using the `Q4_
| [ibm-research/granite-3.2-8b-instruct-Q4_K_M](https://huggingface.co/ibm-research/granite-3.2-8b-instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| | | | |
| [HuggingFaceTB/smollm2-1.7b-instruct-q4_k_m](https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF) | ✓ / ✓ | ✓ / ✓ | ✓ |
| [openbmb/MiniCPM-V-2_6-Q4_K_M](https://huggingface.co/openbmb/MiniCPM-V-2_6-gguf) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/tencent_Hunyuan-7B-Instruct-Q4_K_M](https://huggingface.co/bartowski/tencent_Hunyuan-7B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct-Q4_K_M](https://huggingface.co/LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/prism-ml_Bonsai-8B-unpacked-Q4_K_M](https://huggingface.co/bartowski/prism-ml_Bonsai-8B-unpacked-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [openbmb/MiniCPM-V-2_6-Q4_K_M](https://huggingface.co/openbmb/MiniCPM-V-2_6-gguf) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/tencent_Hunyuan-7B-Instruct-Q4_K_M](https://huggingface.co/bartowski/tencent_Hunyuan-7B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct-Q4_K_M](https://huggingface.co/LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| [bartowski/prism-ml_Bonsai-8B-unpacked-Q4_K_M](https://huggingface.co/bartowski/prism-ml_Bonsai-8B-unpacked-GGUF) | ✓ / ✓ | ✓ / | ✓ |
| | | | |
| [gpustack/bge-m3-Q4_K_M.gguf](https://huggingface.co/gpustack/bge-m3-GGUF) | ✓ | ✗ | ✗ |
@@ -217,18 +222,18 @@ cmake --build build\ReleaseOV --parallel
> [!NOTE]
> The Windows install path is `C:\Intel\openvino` (no spaces) to avoid quoting problems some CMake/Ninja toolchains have with `C:\Program Files (x86)\...`. Adjust to wherever you installed OpenVINO Runtime. From `cmd`, run `C:\Intel\openvino\setupvars.bat`; from PowerShell, run `& "C:\Intel\openvino\setupvars.ps1"` instead. Once the build is finished you can launch the binaries from any `cmd` or `PowerShell` window after sourcing the matching `setupvars` script for that shell.
#### Automated Ubuntu Build Script
#### Ubuntu Build Script
For Ubuntu24 users, the following shell script automates the prerequisite installs (build tools, OpenCL ICD), the OpenVINO Runtime download/extract/setup, and the Ninja-based llama.cpp build.
Save the following as `ubuntu-llamacpp-ov-install.sh` next to where you want the `llama.cpp` folder to land, then run it:
Save the following as `build-llamacpp-ov.sh` next to where you want the `llama.cpp` folder to land, then run it:
```bash
chmod +x ubuntu-llamacpp-ov-install.sh
./ubuntu-llamacpp-ov-install.sh
chmod +x build-llamacpp-ov.sh
./build-llamacpp-ov.sh
```
<details>
<summary>Click to expand <code>ubuntu-llamacpp-ov-install.sh</code></summary>
<summary>Click to expand <code>build-llamacpp-ov.sh</code></summary>
```bash
#!/usr/bin/env bash
@@ -237,8 +242,8 @@ chmod +x ubuntu-llamacpp-ov-install.sh
# ============================================
set -euo pipefail
OPENVINO_VERSION_MAJOR="2026.3"
OPENVINO_VERSION_FULL="2026.3.0.22451.bd8d6542e3c"
OPENVINO_VERSION_MAJOR="2026.3.1"
OPENVINO_VERSION_FULL="2026.3.1.22476.56d9685302d"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OPENVINO_INSTALL_DIR="/opt/intel/openvino_${OPENVINO_VERSION_MAJOR}"
@@ -313,8 +318,9 @@ fi
echo "============================================"
echo "Configuring with CMake..."
echo "============================================"
# shellcheck disable=SC1091
set +u
source "${OPENVINO_ROOT}/setupvars.sh"
set -u
cmake -B build/ReleaseOV -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
@@ -334,27 +340,27 @@ echo " ./build/ReleaseOV/bin/llama-cli -m model.gguf"
```
> [!NOTE]
> The script pins OpenVINO `2026.3` via the `OPENVINO_VERSION_MAJOR` / `OPENVINO_VERSION_FULL` variables at the top — edit them to track a different release.
> The script pins OpenVINO `2026.3.1` via the `OPENVINO_VERSION_MAJOR` / `OPENVINO_VERSION_FULL` variables at the top — edit them to track a different release.
</details>
#### Automated Windows Build Script
#### Windows Build Script
For Windows users, the following `.bat` script automates the prerequisite installs (Git, Ninja, CMake, Visual Studio 2022 Build Tools, vcpkg + OpenCL), the OpenVINO Runtime download/extract, and the Ninja-based llama.cpp build.
Save the following as `windows-llamacpp-ov-install.bat` next to where you want the `llama.cpp` to land, then run it from either **Command Prompt** or **PowerShell**:
Save the following as `build-llamacpp-ov.bat` next to where you want the `llama.cpp` to land, then run it from either **Command Prompt** or **PowerShell**:
```cmd
:: Command Prompt
windows-llamacpp-ov-install.bat
build-llamacpp-ov.bat
```
```powershell
# PowerShell
.\windows-llamacpp-ov-install.bat
.\build-llamacpp-ov.bat
```
<details>
<summary>Click to expand <code>windows-llamacpp-ov-install.bat</code></summary>
<summary>Click to expand <code>build-llamacpp-ov.bat</code></summary>
```bat
@echo off
@@ -364,8 +370,8 @@ REM ============================================
REM llama.cpp OpenVINO Build Script (Ninja)
REM ============================================
set "OPENVINO_VERSION_MAJOR=2026.3"
set "OPENVINO_VERSION_FULL=2026.3.0.22451.bd8d6542e3c"
set "OPENVINO_VERSION_MAJOR=2026.3.1"
set "OPENVINO_VERSION_FULL=2026.3.1.22476.56d9685302d"
set "SCRIPT_DIR=%~dp0"
set "VCPKG_DIR=C:\vcpkg"
@@ -453,9 +459,6 @@ if exist "%OPENVINO_INSTALL_DIR%\setupvars.bat" (
)
REM Move the single top-level folder contents into the versioned install dir.
REM NOTE: delayed expansion (!VAR!) is required because the surrounding else( ... )
REM block is parsed once up-front, so %OPENVINO_EXTRACTED% would expand to "" here
REM and xcopy would then treat "\*" as C:\* and fail with "Cannot perform a cyclic copy".
set "OPENVINO_EXTRACTED="
for /d %%i in ("%OPENVINO_EXTRACT_TMP%\*") do set "OPENVINO_EXTRACTED=%%i"
if not defined OPENVINO_EXTRACTED (
@@ -547,7 +550,7 @@ endlocal
```
> [!NOTE]
> The script pins OpenVINO `2026.3` via the `OPENVINO_VERSION_MAJOR` / `OPENVINO_VERSION_FULL` variables at the top — edit them to track a different release. From any new shell, source the matching `setupvars` script via the junction — `call "C:\Intel\openvino\setupvars.bat"` from `cmd`, or `& "C:\Intel\openvino\setupvars.ps1"` from PowerShell. If `winget` cannot register Visual Studio Build Tools on first run, install them once manually and re-run the script from an elevated **Developer Command Prompt for VS 2022**.
> The script pins OpenVINO `2026.3.1` via the `OPENVINO_VERSION_MAJOR` / `OPENVINO_VERSION_FULL` variables at the top — edit them to track a different release. From any new shell, source the matching `setupvars` script via the junction — `call "C:\Intel\openvino\setupvars.bat"` from `cmd`, or `& "C:\Intel\openvino\setupvars.ps1"` from PowerShell. If `winget` cannot register Visual Studio Build Tools on first run, install them once manually and re-run the script from an elevated **Developer Command Prompt for VS 2022**.
</details>
@@ -712,6 +715,7 @@ Boolean flags follow a uniform convention: set to a **positive integer** (e.g. `
| `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_NPU_COMPILE_CONFIG` | String | `not set` | NPU-only compiler mode parameters forwarded to OpenVINO as `NPU_COMPILATION_MODE_PARAMS`, for example `optimization-level=3`. |
| `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. |
@@ -725,9 +729,11 @@ Boolean flags follow a uniform convention: set to a **positive integer** (e.g. `
| `GGML_OPENVINO_DEBUG_INPUT` | Boolean | `0` | Enable input debugging and print input tensor info. |
| `GGML_OPENVINO_DEBUG_OUTPUT` | Boolean | `0` | Enable output debugging and print output tensor info. |
| `GGML_OPENVINO_PRINT_CGRAPH_TENSOR_ADDRESS` | Boolean | `0` | Print tensor address map once. |
| `GGML_OPENVINO_LOG_UNSUPPORTED_OPS`| Boolean | `0` | Log warning messages with tensor details and rejection reasons for any ops not supported by the OpenVINO backend. Emits at `WARN` level (requires `--log-verbosity >= 2`, enabled by default). |
> [!NOTE]
>`GGML_OPENVINO_STATEFUL_EXECUTION` is an **Experimental** feature to allow stateful execution for managing the KV cache internally inside the OpenVINO model, improving performance on CPUs and GPUs. Stateful execution is not effective on NPUs, and not all models currently support this feature. This feature is experimental and has been validated only with the llama-simple, llama-cli, llama-bench, and llama-run applications and is recommended to enable for the best performance. Other applications, such as llama-server and llama-perplexity, are not yet supported.
> - `GGML_OPENVINO_STATEFUL_EXECUTION` is an **Experimental** feature to allow stateful execution for managing the KV cache internally inside the OpenVINO model, improving performance on CPUs and GPUs. Stateful execution is not effective on NPUs, and not all models currently support this feature. This feature is experimental and has been validated only with the llama-simple, llama-cli, llama-bench, and llama-run applications and is recommended to enable for the best performance. Other applications, such as llama-server and llama-perplexity, are not yet supported.
> - `GGML_OPENVINO_LOG_UNSUPPORTED_OPS` emits logs at `WARN` level (`GGML_LOG_WARN`), which requires application log verbosity `--log-verbosity >= 2` (or `-lv 2`).
### Example Usage
+9
View File
@@ -212,6 +212,15 @@ Use `--backend-sampling` to run supported target-model samplers on the model bac
Unsupported samplers and device layouts fall back to CPU sampling. Tensor split mode does not support backend sampling. A fixed seed produces repeatable random draws, but stochastic CPU and backend sampling can still select different tokens because floating-point operations can differ between implementations and devices. Use greedy sampling when exact output matching is required.
### Synthetic Acceptance
`llama-server` and `llama-cli` can replace normal speculative verification with synthetic decisions for benchmarking. The generated output is not valid model output because accepted draft tokens do not have to match the target model.
Use exactly one of these options:
- `--spec-synth-rates P0,P1,...` sets unconditional per-position acceptance probabilities. Entry `i` is the probability that the first `i+1` draft tokens are all accepted. The number of entries must match the effective maximum draft length. Values must be finite, within `[0, 1]`, and monotonically non-increasing.
- `--spec-synth-len L` sets the target mean acceptance length, including the target token. For `K` maximum draft tokens, `L` must be within `[1, K+1]`. The server finds a constant conditional probability `p` such that `p + p^2 + ... + p^K = L - 1`, then uses unconditional rates `[p, p^2, ..., p^K]`.
### General Speculative Parameters
```
+4
View File
@@ -4643,6 +4643,7 @@ static htp_op_code op_remap_to_htp(const ggml_tensor * t) {
case GGML_OP_CLAMP: return HTP_OP_CLAMP;
case GGML_OP_SQR: return HTP_OP_SQR;
case GGML_OP_SQRT: return HTP_OP_SQRT;
case GGML_OP_LOG: return HTP_OP_UNARY_LOG;
case GGML_OP_SOFT_MAX: return HTP_OP_SOFTMAX;
case GGML_OP_SSM_CONV: return HTP_OP_SSM_CONV;
case GGML_OP_GATED_DELTA_NET: return HTP_OP_GATED_DELTA_NET;
@@ -4666,6 +4667,7 @@ static htp_op_code op_remap_to_htp(const ggml_tensor * t) {
case GGML_UNARY_OP_EXP: return HTP_OP_UNARY_EXP;
case GGML_UNARY_OP_SOFTPLUS: return HTP_OP_UNARY_SOFTPLUS;
case GGML_UNARY_OP_TANH: return HTP_OP_UNARY_TANH;
case GGML_UNARY_OP_ABS: return HTP_OP_UNARY_ABS;
default:
break;
}
@@ -5463,6 +5465,7 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons
case GGML_OP_SQR:
case GGML_OP_SQRT:
case GGML_OP_LOG:
supp = ggml_hexagon_supported_unary(sess, op);
break;
@@ -5481,6 +5484,7 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons
case GGML_UNARY_OP_SIGMOID:
case GGML_UNARY_OP_SOFTPLUS:
case GGML_UNARY_OP_TANH:
case GGML_UNARY_OP_ABS:
case GGML_UNARY_OP_SILU:
case GGML_UNARY_OP_GELU:
case GGML_UNARY_OP_GELU_QUICK:
+2
View File
@@ -62,6 +62,8 @@ enum htp_op_code {
HTP_OP_UNARY_NEG,
HTP_OP_UNARY_SOFTPLUS,
HTP_OP_UNARY_TANH,
HTP_OP_UNARY_ABS,
HTP_OP_UNARY_LOG,
HTP_OP_GLU_SWIGLU,
HTP_OP_GLU_SWIGLU_OAI,
HTP_OP_GLU_GEGLU,
+28
View File
@@ -358,6 +358,34 @@ static inline void hvx_clamp_scalar_f32(uint8_t * restrict dst, const uint8_t *
}
}
//
// Abs
//
static inline void hvx_abs_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) {
assert((unsigned long) dst % 128 == 0);
assert((unsigned long) src % 128 == 0);
HVX_Vector * restrict vdst = (HVX_Vector *) dst;
HVX_Vector * restrict vsrc = (HVX_Vector *) src;
const uint32_t elem_size = sizeof(float);
const uint32_t epv = 128 / elem_size;
const uint32_t nvec = n / epv;
const uint32_t nloe = n % epv;
uint32_t i = 0;
_Pragma("unroll(4)")
for (; i < nvec; i++) {
vdst[i] = hvx_vec_abs_f32(vsrc[i]);
}
if (nloe) {
HVX_Vector v = hvx_vec_abs_f32(vsrc[i]);
hvx_vec_store_a((void *) &vdst[i], nloe * elem_size, v);
}
}
//
// Square
//
+24
View File
@@ -62,4 +62,28 @@ static inline HVX_Vector hvx_vec_log_f32(HVX_Vector x) {
return hvx_vec_add_f32_f32(term_e, res);
}
static inline void hvx_log_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, uint32_t n) {
assert((unsigned long) dst % 128 == 0);
assert((unsigned long) src % 128 == 0);
HVX_Vector * restrict vdst = (HVX_Vector *) dst;
HVX_Vector * restrict vsrc = (HVX_Vector *) src;
const uint32_t elem_size = sizeof(float);
const uint32_t epv = 128 / elem_size;
const uint32_t nvec = n / epv;
const uint32_t nloe = n % epv;
uint32_t i = 0;
_Pragma("unroll(4)")
for (; i < nvec; i++) {
vdst[i] = hvx_vec_log_f32(vsrc[i]);
}
if (nloe) {
HVX_Vector v = hvx_vec_log_f32(vsrc[i]);
hvx_vec_store_a((void *) &vdst[i], nloe * elem_size, v);
}
}
#endif /* HVX_LOG_H */
+2
View File
@@ -777,6 +777,8 @@ static int execute_op(struct htp_ops_context * octx) {
case HTP_OP_UNARY_NEG:
case HTP_OP_UNARY_EXP:
case HTP_OP_UNARY_TANH:
case HTP_OP_UNARY_ABS:
case HTP_OP_UNARY_LOG:
case HTP_OP_L2_NORM:
return op_unary(octx);
+58 -12
View File
@@ -443,6 +443,34 @@ static void tanh_f32(const float * restrict src,
}
}
static void abs_f32(const float * restrict src,
float * restrict dst,
const uint32_t num_rows,
const struct htp_unary_context * uctx) {
htp_unary_op_preamble;
for (uint32_t ir = 0; ir < num_rows; ir++) {
const uint8_t * restrict src_local = (const uint8_t *)src + (ir * src0_row_size_aligned);
uint8_t * restrict dst_local = (uint8_t *)dst + (ir * dst_row_size_aligned);
hvx_abs_f32_aa(dst_local, src_local, ne0);
}
}
static void log_f32(const float * restrict src,
float * restrict dst,
const uint32_t num_rows,
const struct htp_unary_context * uctx) {
htp_unary_op_preamble;
for (uint32_t ir = 0; ir < num_rows; ir++) {
const uint8_t * restrict src_local = (const uint8_t *)src + (ir * src0_row_size_aligned);
uint8_t * restrict dst_local = (uint8_t *)dst + (ir * dst_row_size_aligned);
hvx_log_f32_aa(dst_local, src_local, ne0);
}
}
#define DEFINE_UNARY_TASK(NAME, IS_RMS_NORM_MUL, IS_TRI, CORE_EXPR) \
static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * data) { \
const struct htp_unary_context * uctx = (const struct htp_unary_context *) data; \
@@ -478,6 +506,9 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
const uint32_t nb11 = src1 ? src1->nb[1] : 0; \
const uint32_t nb12 = src1 ? src1->nb[2] : 0; \
const uint32_t nb13 = src1 ? src1->nb[3] : 0; \
const uint32_t nb11_bc = (src1 && src1->ne[1] > 1) ? nb11 : 0; \
const uint32_t nb12_bc = (src1 && src1->ne[2] > 1) ? nb12 : 0; \
const uint32_t nb13_bc = (src1 && src1->ne[3] > 1) ? nb13 : 0; \
const bool src1_contig = src1 ? ((nb12 == (size_t)ne01 * nb11) && (nb13 == (size_t)ne02 * nb12)) : false; \
\
uint8_t * src0_vtcm_data = uctx->vtcm_src0 + (ith * uctx->vtcm_src0_size_per_thread); \
@@ -497,8 +528,12 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
const struct fastdiv_values * div_ne02 = &uctx->kparams->div_ne02; \
const struct fastdiv_values * div_ne012 = &uctx->kparams->div_ne012; \
\
const uint32_t src0_max_block = src0_contig ? uctx->block : MIN((uint32_t)uctx->block, ne01); \
const uint32_t dst_max_block = dst_contig ? uctx->block : MIN((uint32_t)uctx->block, ne1); \
const bool src1_needs_row_clip = (IS_RMS_NORM_MUL) && !uctx->broadcast_weight && !src1_contig; \
const bool block_src0_contig = src0_contig && !src1_needs_row_clip; \
const bool block_dst_contig = dst_contig && !src1_needs_row_clip; \
\
const uint32_t src0_max_block = block_src0_contig ? uctx->block : MIN((uint32_t)uctx->block, ne01); \
const uint32_t dst_max_block = block_dst_contig ? uctx->block : MIN((uint32_t)uctx->block, ne1); \
const uint32_t BLOCK = MIN(src0_max_block, dst_max_block); \
if (BLOCK == 0) { \
FARF(ERROR, "unary-f32 : current VTCM reservation %zu is too small, needed at least %zu\n", \
@@ -515,8 +550,8 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
} \
\
for (uint32_t ir = src0_start_row, vtcm_idx = 0; ir < src0_end_row && vtcm_idx < 2; vtcm_idx++) { \
const uint32_t block_size = unary_block_size(ir, src0_end_row, BLOCK, src0_contig, dst_contig, ne01, \
div_ne01); \
const uint32_t block_size = unary_block_size(ir, src0_end_row, BLOCK, block_src0_contig, block_dst_contig, \
ne01, div_ne01); \
\
dma_queue_push(dma_queue, \
dma_make_ptr(data_dst, dst_vtcm_data + (vtcm_idx * dst_vtcm_half_size)), \
@@ -530,7 +565,7 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
\
if ((IS_RMS_NORM_MUL) && !uctx->broadcast_weight) { \
const size_t src1_off = src1_contig ? (ir * nb11) : \
unary_row_offset(ir, ne01, ne02, div_ne01, div_ne02, div_ne012, nb11, nb12, nb13); \
unary_row_offset(ir, ne01, ne02, div_ne01, div_ne02, div_ne012, nb11_bc, nb12_bc, nb13_bc); \
dma_queue_push(dma_queue, \
dma_make_ptr(src1_vtcm_data + (vtcm_idx * src1_vtcm_half_size), data_src1 + src1_off), \
uctx->src1_row_size_aligned, nb11, uctx->src1_data_row_size, block_size); \
@@ -540,8 +575,8 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
} \
\
for (uint32_t ir = src0_start_row; ir < src0_end_row; ) { \
const uint32_t block_size = unary_block_size(ir, src0_end_row, BLOCK, src0_contig, dst_contig, ne01, \
div_ne01); \
const uint32_t block_size = unary_block_size(ir, src0_end_row, BLOCK, block_src0_contig, block_dst_contig, \
ne01, div_ne01); \
\
float * dst_vtcm = (float *) dma_queue_pop(dma_queue).src; \
float * src0_vtcm = (float *) dma_queue_pop(dma_queue).dst; \
@@ -562,12 +597,12 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
\
const uint32_t next_ir = ir + block_size; \
if (next_ir < src0_end_row) { \
const uint32_t next_block_size = unary_block_size(next_ir, src0_end_row, BLOCK, src0_contig, dst_contig,\
ne01, div_ne01); \
const uint32_t next_block_size = unary_block_size(next_ir, src0_end_row, BLOCK, block_src0_contig, \
block_dst_contig, ne01, div_ne01); \
const uint32_t pref_ir = next_ir + next_block_size; \
if (pref_ir < src0_end_row) { \
const uint32_t pref_block_size = unary_block_size(pref_ir, src0_end_row, BLOCK, src0_contig, \
dst_contig, ne01, div_ne01); \
const uint32_t pref_block_size = unary_block_size(pref_ir, src0_end_row, BLOCK, block_src0_contig, \
block_dst_contig, ne01, div_ne01); \
const size_t src0_pref_off = src0_contig ? (pref_ir * nb01) : \
unary_row_offset(pref_ir, ne01, ne02, div_ne01, div_ne02, div_ne012, nb01, nb02, nb03); \
dma_queue_push(dma_queue, \
@@ -576,7 +611,8 @@ static void unary_task_f32_##NAME(unsigned int nth, unsigned int ith, void * dat
\
if ((IS_RMS_NORM_MUL) && !uctx->broadcast_weight) { \
const size_t src1_pref_off = src1_contig ? (pref_ir * nb11) : \
unary_row_offset(pref_ir, ne01, ne02, div_ne01, div_ne02, div_ne012, nb11, nb12, nb13); \
unary_row_offset(pref_ir, ne01, ne02, div_ne01, div_ne02, div_ne012, nb11_bc, nb12_bc, \
nb13_bc); \
dma_queue_push(dma_queue, \
dma_make_ptr(src1_vtcm, data_src1 + src1_pref_off), \
uctx->src1_row_size_aligned, nb11, uctx->src1_data_row_size, pref_block_size); \
@@ -603,6 +639,8 @@ DEFINE_UNARY_TASK(unary_silu, false, false, silu_f32(src0_vtcm, dst_vtcm, bl
DEFINE_UNARY_TASK(unary_gelu, false, false, gelu_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(unary_softplus, false, false, softplus_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(unary_tanh, false, false, tanh_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(unary_abs, false, false, abs_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(unary_log, false, false, log_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(l2_norm, false, false, l2_norm_f32(src0_vtcm, dst_vtcm, block_size, uctx))
DEFINE_UNARY_TASK(tri, false, true, tri_f32(src0_vtcm, dst_vtcm, block_size, ir, uctx))
@@ -850,6 +888,8 @@ DEFINE_UNARY_TILED_TASK(unary_silu, false, tile_silu_f32(dst_vtcm, src_vtcm,
DEFINE_UNARY_TILED_TASK(unary_gelu, false, tile_gelu_f32(dst_vtcm, src_vtcm, tw))
DEFINE_UNARY_TILED_TASK(unary_softplus, false, tile_unary_softplus_f32(dst_vtcm, src_vtcm, tw))
DEFINE_UNARY_TILED_TASK(unary_tanh, false, hvx_tanh_f32_aa(dst_vtcm, src_vtcm, tw))
DEFINE_UNARY_TILED_TASK(unary_abs, false, hvx_abs_f32_aa(dst_vtcm, src_vtcm, tw))
DEFINE_UNARY_TILED_TASK(unary_log, false, hvx_log_f32_aa(dst_vtcm, src_vtcm, tw))
DEFINE_UNARY_TILED_TASK(tri, true, tri_apply_tile_f32(src_vtcm, dst_vtcm, tw, col, i01, ne0, tri_ttype))
static int execute_op_unary_f32(struct htp_ops_context * octx) {
@@ -875,6 +915,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
case HTP_OP_UNARY_GELU: op_type = "gelu-f32"; break;
case HTP_OP_UNARY_SOFTPLUS: op_type = "softplus-f32"; break;
case HTP_OP_UNARY_TANH: op_type = "tanh-f32"; break;
case HTP_OP_UNARY_ABS: op_type = "abs-f32"; break;
case HTP_OP_UNARY_LOG: op_type = "log-f32"; break;
case HTP_OP_L2_NORM: op_type = "l2norm-f32"; break;
case HTP_OP_TRI: op_type = "tri-f32"; break;
@@ -973,6 +1015,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
case HTP_OP_UNARY_GELU: task_func = unary_task_f32_tiled_unary_gelu; break;
case HTP_OP_UNARY_SOFTPLUS: task_func = unary_task_f32_tiled_unary_softplus; break;
case HTP_OP_UNARY_TANH: task_func = unary_task_f32_tiled_unary_tanh; break;
case HTP_OP_UNARY_ABS: task_func = unary_task_f32_tiled_unary_abs; break;
case HTP_OP_UNARY_LOG: task_func = unary_task_f32_tiled_unary_log; break;
case HTP_OP_TRI: task_func = unary_task_f32_tiled_tri; break;
default: break;
}
@@ -992,6 +1036,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
case HTP_OP_UNARY_GELU: task_func = unary_task_f32_unary_gelu; break;
case HTP_OP_UNARY_SOFTPLUS: task_func = unary_task_f32_unary_softplus; break;
case HTP_OP_UNARY_TANH: task_func = unary_task_f32_unary_tanh; break;
case HTP_OP_UNARY_ABS: task_func = unary_task_f32_unary_abs; break;
case HTP_OP_UNARY_LOG: task_func = unary_task_f32_unary_log; break;
case HTP_OP_L2_NORM: task_func = unary_task_f32_l2_norm; break;
case HTP_OP_TRI: task_func = unary_task_f32_tri; break;
default: break;
+2
View File
@@ -55,6 +55,8 @@ static inline bool htp_op_is_unary(uint32_t opcode) {
case HTP_OP_UNARY_GELU:
case HTP_OP_UNARY_SOFTPLUS:
case HTP_OP_UNARY_TANH:
case HTP_OP_UNARY_ABS:
case HTP_OP_UNARY_LOG:
case HTP_OP_L2_NORM:
case HTP_OP_TRI:
return true;
+363 -1
View File
@@ -66,6 +66,7 @@ fa_vec_cfg_t fa_vec_baseline_cfg(int dk, int dv) {
// One row per kept bucket, plus per-(dtype,dk,dv) ne11-collapsed domain defaults
// (ne11_b = FA_VEC_NE11_DEFAULT, ne01_b = domain). To retune or add a device, re-run the
// sweep and paste its output. See ggml-metal-tuning.h for the row/lookup semantics.
// ref: https://github.com/ggml-org/llama.cpp/pull/27824
constexpr fa_vec_entry_t fa_vec_tuned_table[] = {
{ { GGML_METAL_DEVICE_M1_PRO, GGML_TYPE_F16, 32, 32, 3, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M1_PRO, GGML_TYPE_F16, 64, 64, -1, 0 }, { 1, 4 } },
@@ -449,6 +450,159 @@ constexpr fa_vec_entry_t fa_vec_tuned_table[] = {
{ { GGML_METAL_DEVICE_M2_ULTRA, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M2_ULTRA, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 1, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 32, 32, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, 3, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, 3, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, 3, 3 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 96, 96, 3, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 128, 128, 2, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 192, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 128, 1, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 128, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 192, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 256, 256, 2, 3 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 320, 256, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 320, 256, 3, 0 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 2, 0 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 3, 0 }, { 4, 1 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 1, 3 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 2, 1 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 2, 3 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 3, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 512, 512, 3, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 576, 512, 2, 0 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 576, 512, 2, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 576, 512, 2, 3 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_F16, 576, 512, 3, 1 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 64, 64, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 96, 96, 3, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 128, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 192, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 192, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 576, 512, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 576, 512, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 576, 512, 1, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M3_MAX, GGML_TYPE_Q8_0, 576, 512, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 32, 32, 1, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 32, 32, 2, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 32, 32, 3, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 64, 64, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 96, 96, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 96, 96, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 192, 128, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 256, 256, 3, 0 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 320, 256, 3, 0 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_F16, 512, 512, 3, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 128, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 128, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 192, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 512, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 512, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, 1, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, 1, 3 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_PRO, GGML_TYPE_Q8_0, 576, 512, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M4_MAX, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_MAX, GGML_TYPE_F16, 32, 32, 2, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M4_MAX, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } },
@@ -640,7 +794,215 @@ constexpr fa_vec_entry_t fa_vec_tuned_table[] = {
{ { GGML_METAL_DEVICE_M4_MAX, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M4_MAX, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_MAX, GGML_TYPE_F16, 32, 32, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 1, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 32, 32, 3, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, -1, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 64, 64, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 96, 96, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 96, 96, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 3, 0 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 1, 2 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 3, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 128, 128, 3, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 192, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 3, 2 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 192, 128, 3, 3 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 256, 256, 1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 256, 256, 1, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 320, 256, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 320, 256, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 320, 256, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 320, 256, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 1, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 2, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 2, 3 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 3, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 3, 2 }, { 4, 1 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 3, 3 }, { 4, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 512, 512, 3, 4 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 576, 512, 2, 0 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 576, 512, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 576, 512, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 576, 512, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_F16, 576, 512, 2, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 32, 32, 3, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, 1, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, 1, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, 2, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 64, 64, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 96, 96, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 96, 96, 3, 4 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 192, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 192, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 192, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 128, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 192, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 320, 256, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 512, 512, 3, 0 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 512, 512, 2, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 512, 512, 3, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 2, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 32, 32, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 64, 64, 1, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 64, 64, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 64, 64, 3, 4 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 128, 128, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 128, 128, 1, 4 }, { 1, 1 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 192, 192, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 192, 192, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 192, 128, 2, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 192, 128, 3, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 256, 256, -1, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 256, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 256, 256, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 320, 256, 3, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 320, 256, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 320, 256, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 512, 512, 2, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 512, 512, 2, 3 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 512, 512, 3, 3 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, -1, 1 }, { 2, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, 1, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, 1, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, 1, 3 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, 1, 4 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_F16, 576, 512, 2, 2 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 32, 32, -1, 1 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 32, 32, 1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 32, 32, 1, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 32, 32, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 32, 32, 3, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 64, 64, 1, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 96, 96, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 128, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 128, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, 1, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, 2, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, 2, 3 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, 2, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 192, 3, 4 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 192, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 256, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 320, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 512, 512, 1, 0 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 512, 512, -1, 1 }, { 1, 2 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 512, 512, 2, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q4_0, 512, 512, 2, 3 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 1, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 1, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 2, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 32, 32, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 64, 64, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 64, 64, 3, 2 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 64, 64, 3, 3 }, { 4, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 96, 96, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 96, 96, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 96, 96, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 128, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 128, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 128, 128, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 192, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 192, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 192, 2, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 192, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 128, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 128, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 192, 128, 3, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 256, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 320, 256, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 0 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_PRO, GGML_TYPE_Q8_0, 576, 512, -1, 1 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_MAX, GGML_TYPE_F16, 32, 32, -1, 1 }, { 2, 4 } },
{ { GGML_METAL_DEVICE_M5_MAX, GGML_TYPE_F16, 32, 32, 1, 2 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_MAX, GGML_TYPE_F16, 32, 32, 1, 4 }, { 1, 4 } },
{ { GGML_METAL_DEVICE_M5_MAX, GGML_TYPE_F16, 32, 32, 2, 2 }, { 4, 4 } },
+53 -3
View File
@@ -903,6 +903,8 @@ struct ggml_backend_opencl_context {
cl_kernel kernel_gemv_moe_mxfp4_f32_ns_wimg = nullptr; // weight-as-texture MoE decode GEMV
cl_kernel kernel_gemm_moe_mxfp4_q8_1_dp4a = nullptr; // dp4a (int8) mxfp4 MoE prefill GEMM
cl_kernel kernel_gemm_moe_q4_0_q8_1_dp4a = nullptr; // dp4a (int8) q4_0 MoE prefill GEMM
cl_kernel kernel_gemm_moe_mxfp4_q8_1_dp4a_bin = nullptr; // binary dp4a (int8) mxfp4 MoE prefill GEMM
cl_kernel kernel_gemm_moe_q4_0_q8_1_dp4a_bin = nullptr; // binary dp4a (int8) q4_0 MoE prefill GEMM
cl_kernel kernel_moe_reorder_b;
cl_kernel kernel_moe_histogram, kernel_moe_scan, kernel_moe_fill, kernel_moe_scatter;
cl_kernel kernel_moe_scatter_stable = nullptr; // deterministic slot assignment
@@ -4248,6 +4250,24 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
GGML_LOG_CONT(".");
}
// gemm_moe_mxfp4_q8_1_dp4a_bin (dp4a prefill GEMM)
if (backend_ctx->has_integer_dot) {
size_t bin_size = 0;
backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a_bin = nullptr;
if (use_adreno_bin_kernels(backend_ctx)) {
const char * kernel_bin = (const char *)backend_ctx->get_adreno_bin_kernel("gemm_moe_mxfp4_q8_1_dp4a_ila", &bin_size);
if (kernel_bin && bin_size > 0) {
cl_program prog =
build_program_from_binary(backend_ctx->context, backend_ctx->device, kernel_bin, CL_moe_compile_opts, bin_size);
CL_CHECK((backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a_bin = clCreateKernel(prog, "kernel_gemm_moe_mxfp4_q8_1_dp4a_ila", &err), err));
CL_CHECK(clReleaseProgram(prog));
GGML_LOG_CONT(".");
}
}
}
// gemm_moe_q4_0_q8_1_dp4a (dp4a prefill GEMM)
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
@@ -4265,6 +4285,24 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
GGML_LOG_CONT(".");
}
// gemm_moe_q4_0_q8_1_dp4a_bin (dp4a prefill GEMM)
if (backend_ctx->has_integer_dot) {
size_t bin_size = 0;
backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin = nullptr;
if (use_adreno_bin_kernels(backend_ctx)) {
const char * kernel_bin = (const char *)backend_ctx->get_adreno_bin_kernel("gemm_moe_q4_0_q8_1_dp4a_ila", &bin_size);
if (kernel_bin && bin_size > 0) {
cl_program prog =
build_program_from_binary(backend_ctx->context, backend_ctx->device, kernel_bin, CL_moe_compile_opts, bin_size);
CL_CHECK((backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin = clCreateKernel(prog, "kernel_gemm_moe_q4_0_q8_1_dp4a_ila", &err), err));
CL_CHECK(clReleaseProgram(prog));
GGML_LOG_CONT(".");
}
}
}
// gemm_moe_q8_1_dp4a (generic dp4a MoE GEMM; MOE_QT=80 -> q8_0 expert variant)
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
@@ -21519,7 +21557,9 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
// bin kernel takes precedence
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin == nullptr;
if (backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin == nullptr) {
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin == nullptr;
}
cl_buffer_region region;
region.origin = 0;
@@ -21625,6 +21665,10 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
// dp4a GEMM
cl_kernel dk = backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a;
if (backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin) {
dk = backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a_bin;
}
int aidx = 0;
CL_CHECK(clSetKernelArg(dk, aidx++, sizeof(cl_mem), &extra0_q4_0->q_img));
CL_CHECK(clSetKernelArg(dk, aidx++, sizeof(cl_mem), &extra0_q4_0->d));
@@ -23463,8 +23507,10 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
// bin kernel takes precedence
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_mxfp4_f32_ns_bin == nullptr;
// bin kernel takes precedence, dp4a bin kernel has higher priority than normal bin kernel
if (backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a_bin == nullptr) {
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_mxfp4_f32_ns_bin == nullptr;
}
cl_buffer_region region;
region.origin = 0;
@@ -23573,6 +23619,10 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
// dp4a GEMM
cl_kernel dk = backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a;
if (backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a_bin) {
dk = backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a_bin;
}
int aidx = 0;
CL_CHECK(clSetKernelArg(dk, aidx++, sizeof(cl_mem), &extra0_mxfp4->q_img));
CL_CHECK(clSetKernelArg(dk, aidx++, sizeof(cl_mem), &extra0_mxfp4->e));
+2
View File
@@ -1,6 +1,8 @@
find_package(OpenVINO REQUIRED COMPONENTS Runtime Threading)
find_package(OpenCL REQUIRED)
message(STATUS "Found OpenVINO: ${OpenVINO_DIR} (found version \"${OpenVINO_VERSION}\")")
file(GLOB_RECURSE GGML_HEADERS_OPENVINO "*.h" "*.hpp")
file(GLOB_RECURSE GGML_SOURCES_OPENVINO "*.cpp")
+136 -17
View File
@@ -357,6 +357,18 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
break;
}
case GGML_OP_VIEW: {
if (m_is_static && node->src[0] != nullptr &&
(node->src[0]->op == GGML_OP_GATED_DELTA_NET || node->src[0]->op == GGML_OP_CONCAT)) {
// VIEW slicing a GATED_DELTA_NET combined [attn|state] output, or the conv_input
// CONCAT. The consuming CPY/RMS_NORM op recovers the true window at runtime via
// ssm_state_size / the fixed conv kernel width, so this VIEW must stay an identity
// pass-through of the full source here too (it already is on the dynamic path);
// otherwise the generic static-mode Slice below would bake in the *captured*
// cgraph's token count, which is wrong once the compiled static model runs with a
// different token count (prefill chunk size or 1).
op_case = 1;
break;
}
if (node->src[0]->op == GGML_OP_VIEW) {
auto * src = node->src[0];
if (ggml_nelements(node) != ggml_nelements(src)) {
@@ -408,6 +420,23 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
}
break;
}
case GGML_OP_POOL_2D: {
const ggml_op_pool pool_mode = static_cast<ggml_op_pool>(node->op_params[0]);
switch (pool_mode) {
case GGML_OP_POOL_MAX: {
op_case = 1;
break;
}
case GGML_OP_POOL_AVG: {
op_case = 2;
break;
}
default:
op_case = 0;
break;
}
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) {
@@ -425,6 +454,31 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
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;
} else if (node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src != nullptr) {
// op_case 5: KV write for decoder self-attention (dynamic write offset)
// op_case 6: KV write for encoder self-attn or cross-attn (static offset)
const ggml_tensor * kv_buf = node->src[1]->view_src;
if (kv_buf->ne[1] == 1 && kv_buf->ne[2] == 1 && kv_buf->ne[3] == 1) {
op_case = 6;
// Forward-scan the graph for a FLASH_ATTN_EXT that reads from
// the same buffer. Having a mask (src[3] != nullptr) implies
// decoder self-attention and the write offset is dynamic.
for (int i = 0; i < m_cgraph->n_nodes; i++) {
const ggml_tensor * n = m_cgraph->nodes[i];
if (n->op != GGML_OP_FLASH_ATTN_EXT) {
continue;
}
// K (src[1]) and V (src[2]) are 3-D views whose view_src is
// the flat KV buffer we are writing to.
if ((n->src[1] != nullptr && n->src[1]->view_src == kv_buf) ||
(n->src[2] != nullptr && n->src[2]->view_src == kv_buf)) {
if (n->src[3] != nullptr) {
op_case = 5; // decoder self-attention: mask present
}
break;
}
}
}
}
break;
}
@@ -448,6 +502,15 @@ int GgmlOvDecoder::compute_op_case(const ggml_tensor * node) const {
}
break;
}
case GGML_OP_FLASH_ATTN_EXT: {
if (node->src[1] != nullptr && node->src[1]->op == GGML_OP_VIEW && node->src[1]->view_src != nullptr) {
const ggml_tensor * kv_buf = node->src[1]->view_src;
if (kv_buf->ne[1] == 1 && kv_buf->ne[2] == 1 && kv_buf->ne[3] == 1) {
op_case = (node->src[3] != nullptr) ? 1 : 2;
}
}
break;
}
default:
break;
}
@@ -479,23 +542,35 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
switch (node->op) {
case GGML_OP_FLASH_ATTN_EXT:
if (node->src[0] == nullptr || node->src[1] == nullptr || node->src[3] == nullptr) {
if (node->src[0] == nullptr || node->src[1] == nullptr) {
return -1;
}
switch (node->src[1]->op) {
case GGML_OP_PERMUTE:
// case 0: node op is FLASH_ATTN_EXT, src 1 not null & op is PERMUTE & the permuted tensor src is the view of cache k
if (node->src[1]->src[0] != nullptr && node->src[1]->src[0]->op == GGML_OP_VIEW) {
// case 0: src[1] is PERMUTE of a cache VIEW, mask required
if (node->src[3] != nullptr && node->src[1]->src[0] != nullptr &&
node->src[1]->src[0]->op == GGML_OP_VIEW) {
return 0;
}
break;
case GGML_OP_CPY:
// case 1: node op is FLASH_ATTN_EXT, src 1 not null & op is CPY & the copied tensor src is PERMUTE & the permuted tensor src is the view of cache k
if (node->src[1]->src[0] != nullptr && node->src[1]->src[0]->op == GGML_OP_PERMUTE &&
node->src[1]->src[0]->src[0] != nullptr && node->src[1]->src[0]->src[0]->op == GGML_OP_VIEW) {
// case 1: src[1] is CPY of a PERMUTE(VIEW), mask required
if (node->src[3] != nullptr && node->src[1]->src[0] != nullptr &&
node->src[1]->src[0]->op == GGML_OP_PERMUTE && node->src[1]->src[0]->src[0] != nullptr &&
node->src[1]->src[0]->src[0]->op == GGML_OP_VIEW) {
return 1;
}
break;
case GGML_OP_VIEW:
// cases 4/5/6: whisper - K is a direct non-contiguous VIEW_3D of a KV cache
if (node->src[1]->view_src != nullptr) {
if (node->src[3] != nullptr) {
return 4; // decoder self-attention
} else {
return 5; // cross-attention or encoder self-attention
};
}
break;
default:
break;
}
@@ -548,6 +623,18 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
cache_k_permute = node->src[0]->src[0]->src[0];
mask = node->src[1];
break;
case 4:
case 5: {
// whisper: K is a direct VIEW_3D of the KV buffer, no PERMUTE node
auto * cache_k_view = node->src[1]; // VIEW_3D of kv_self.k or kv_cross.k`
compute_params.token_len_per_seq = node->src[0]->ne[1];
if (attention_pattern_case == 4) {
compute_params.attention_size = cache_k_view->ne[1];
} else {
compute_params.attention_size_static = cache_k_view->ne[1];
}
continue;
}
default:
break;
}
@@ -654,10 +741,8 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
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;
@@ -718,11 +803,15 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
} else if (is_kvcache(input, op)) {
// kvcache
input_shape = ov::PartialShape{get_shape(input)};
if (!m_is_static) {
// Whisper.cpp uses a fixed size 1D KV buffer [N, 1, 1, 1] (GGML) or [1, 1, 1, N] (OV).
// the token fill level is handled by token_len_per_seq + dynamic mask input.
// skip dynamic dim and stateful reshape for this layout.
const bool is_flat_kv = (input->ne[1] == 1 && input->ne[2] == 1 && input->ne[3] == 1);
if (!m_is_static && !is_flat_kv) {
// do not fix ctx size to make llama-bench work across test params
input_shape[2] = -1;
}
if (is_stateful()) {
if (is_stateful() && !is_flat_kv) {
// Convert stateless KV cache layout [1, 1, seq, n_heads_kv * head_size]
// to stateful layout [1, seq, n_heads_kv, head_size].
assert(input_shape.size() == 4 && input_shape[0] == 1 && input_shape[1] == 1 &&
@@ -738,7 +827,9 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
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};
// On NPU the total slot count (n_seq_max) is fixed at translation time, so the s_copy
// index list has a static length; on CPU/GPU it may change across compiles (defrag).
input_shape = m_is_static ? ov::PartialShape{get_shape(input)} : ov::PartialShape{1, 1, 1, -1};
} else {
input_shape = ov::PartialShape{get_shape(input)};
@@ -790,13 +881,16 @@ void GgmlOvDecoder::add_extra_inputs() {
// see llama_kv_cache_unified::get_n_kv and llama_kv_cache_unified::get_padding.
// 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) {
m_model_extra_inputs[name] = {ov::element::i64, ov::Shape{1}, value, !m_is_static};
auto create_1d_input = [this](const std::string & name, int64_t value, bool force_parameter = false) {
m_model_extra_inputs[name] = {ov::element::i64, ov::Shape{1}, value, force_parameter || !m_is_static};
};
if (m_compute_params.attention_size != -1) {
create_1d_input("attention_size", m_compute_params.attention_size);
}
if (m_compute_params.attention_size_static != -1) {
create_1d_input("attention_size_static", m_compute_params.attention_size_static);
}
if (m_compute_params.attention_size_swa != -1) {
create_1d_input("attention_size_swa", m_compute_params.attention_size_swa);
}
@@ -809,17 +903,32 @@ void GgmlOvDecoder::add_extra_inputs() {
// 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);
// Whether/which cache slot to reset varies per compute call (e.g. a new sequence starting
// vs. continued decoding). can_reuse_statically() does not invalidate the cached static
// model on ComputeParams changes, so these must stay runtime Parameters even when static
// (scale.cpp op_case 1 only uses them in value comparisons, never as Slice bounds, so this
// does not reintroduce dynamic shapes).
create_1d_input("cache_rs_reset_idx", m_compute_params.cache_rs_reset_idx, /*force_parameter=*/true);
create_1d_input("cache_rs_reset_len", m_compute_params.cache_rs_reset_len, /*force_parameter=*/true);
}
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);
if (m_is_static) {
// Number of real tokens in the current prefill chunk. The last chunk is padded with
// fabricated token ids; attention masks them out, but the recurrent (GDN/conv) path
// would otherwise fold them into cache_r/cache_s permanently. Varies per chunk, so it
// must stay a runtime Parameter; it is only compared against a Range or used as Gather
// indices, so it does not make any shape dynamic.
create_1d_input("chunk_valid_len", get_static_n_tokens(), /*force_parameter=*/true);
}
}
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);
if (!m_is_static) {
create_1d_input("rs_src_begin_" + node_name, writeback.src_begin);
}
}
}
@@ -1785,13 +1894,23 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
auto dynamic_dim_stride = src_logical_nb[dynamic_dim_idx] / ggml_type_size(node->src[0]->type) *
ggml_type_size(node->type);
int matched_dim_count = 0;
int first_matched_dim = -1;
for (int i = 0; i < GGML_MAX_DIMS; i++) {
if (node->nb[i] == dynamic_dim_stride && node->ne[i] == node->src[0]->ne[dynamic_dim_idx]) {
if (first_matched_dim == -1) {
first_matched_dim = i;
}
m_node_dynamic_dims[node] = i;
matched_dim_count++;
}
}
if (matched_dim_count != 1) {
if (matched_dim_count > 1 && node->src[0]->ne[dynamic_dim_idx] == 1) {
// Single-token capture: every trailing dim is size 1 with the same stride, so
// the match is ambiguous. The lowest index is the real axis; the rest are
// ggml's size-1 padding. Bailing out here would bake the captured token count
// into the static prefill model, which then runs with a different one.
m_node_dynamic_dims[node] = first_matched_dim;
} else if (matched_dim_count != 1) {
m_node_dynamic_dims[node] = -1;
GGML_LOG_WARN("ggml-openvino: cannot determine dynamic dim for CONT node '%s', src[0]: '%s'\n",
node->name, node->src[0]->name);
+7 -5
View File
@@ -47,6 +47,7 @@ struct ComputeParams {
int seq_active_start = 0;
int attention_size = -1;
int attention_size_swa = -1;
int attention_size_static = -1; // encoder/cross-attn KV fill level (whisper)
int input_len = -1;
int token_len_per_seq = -1;
int past_kv_len = -1;
@@ -84,14 +85,15 @@ struct ComputeParams {
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)
int src_begin = 0; // first source row or column copied by the CPY
};
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.
// Destination slot offset of each state cache writeback CPY node, keyed by node name. It
// changes with the batch (kv head, active sequence count) and, with rollback enabled
// (cparams.n_rs_seq > 0), the conv state is written back once per snapshot slot. Passed to the
// cached model as a runtime input. Dynamic models also receive the source-side offset; static
// models use a fixed end-anchored offset in the translator.
};
class GgmlOvDecoder : public ov::frontend::ggml::GgmlDecoder {
+11 -1
View File
@@ -32,6 +32,8 @@ void ggml_openvino_device_config::init() {
"GGML_OPENVINO_DEVICE",
"GGML_OPENVINO_CACHE_DIR",
"GGML_OPENVINO_DEBUG_NODE",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR",
"GGML_OPENVINO_NPU_COMPILE_CONFIG",
// Integer values (use ggml_openvino_getenv_int)
"GGML_OPENVINO_PREFILL_CHUNK_SIZE",
// Boolean toggles (treated as int flags via ggml_openvino_getenv_int)
@@ -41,6 +43,9 @@ void ggml_openvino_device_config::init() {
"GGML_OPENVINO_DUMP_IR",
"GGML_OPENVINO_DEBUG_INPUT",
"GGML_OPENVINO_DEBUG_OUTPUT",
// Force the static (NPU-shape) compute path on any device, e.g. GGML_OPENVINO_DEVICE=CPU,
// to test the static-shape translation without NPUW/real NPU hardware in the loop.
"GGML_OPENVINO_FORCE_STATIC",
"GGML_OPENVINO_PRINT_CGRAPH_TENSOR_ADDRESS",
"GGML_OPENVINO_ENABLE_CACHE",
"GGML_OPENVINO_DISABLE_CACHE",
@@ -50,7 +55,7 @@ void ggml_openvino_device_config::init() {
"GGML_OPENVINO_MEMORY_OPTIMIZE",
"GGML_OPENVINO_RELEASE_WEIGHTS",
"GGML_OPENVINO_REDUCE_COMPILE_MEM",
"GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR",
"GGML_OPENVINO_LOG_UNSUPPORTED_OPS",
};
for (const char * const & env_var : env_var_names) {
@@ -85,6 +90,11 @@ void ggml_openvino_device_config::init() {
compile_config["NPUW_CACHE_DIR"] = cache_dir;
compile_config.insert(ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE));
}
const char * compilation_mode_params =
ggml_openvino_getenv_str("GGML_OPENVINO_NPU_COMPILE_CONFIG");
if (compilation_mode_params && strlen(compilation_mode_params) > 0) {
compile_config["NPU_COMPILATION_MODE_PARAMS"] = compilation_mode_params;
}
} else if (cache_dir && strlen(cache_dir) > 0) {
compile_config.insert(ov::cache_dir(cache_dir));
compile_config.insert(ov::cache_mode(ov::CacheMode::OPTIMIZE_SIZE));
+134 -97
View File
@@ -908,11 +908,27 @@ static bool has_non_contiguous_view_input(const ggml_tensor * op) {
}
static bool is_supported_flash_attn_pattern(const ggml_tensor * op) {
// pattern of q,k,v should be q->op==PERMUTE, q->src[0]->op==VIEW, q->src[0]->src[0]->view_src==nullptr
// Each Q/K/V input must follow one of:
// PERMUTE -> VIEW -> base (view_src==nullptr) (llama KV-cache path)
// PERMUTE -> RESHAPE -> base (view_src==nullptr) (whisper Q)
// VIEW -> base (view_src==nullptr) (whisper K/V from kv_pad)
for (int i = 0; i < 3; i++) {
const ggml_tensor * src = op->src[i];
if (src->op != GGML_OP_PERMUTE || src->src[0] == nullptr || src->src[0]->op != GGML_OP_VIEW ||
src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) {
if (src->op == GGML_OP_PERMUTE) {
if (src->src[0] == nullptr) {
return false;
}
if (src->src[0]->op != GGML_OP_VIEW && src->src[0]->op != GGML_OP_RESHAPE) {
return false;
}
if (src->src[0]->src[0] == nullptr || src->src[0]->src[0]->view_src != nullptr) {
return false;
}
} else if (src->op == GGML_OP_VIEW) {
if (src->src[0] == nullptr || src->src[0]->view_src != nullptr) {
return false;
}
} else {
return false;
}
}
@@ -1030,18 +1046,29 @@ static bool is_msa_block_mask_expansion(const ggml_tensor * op) {
return tensor_name_starts_with(src, "msa_block_mask");
}
static bool is_op_unsupported_case(const ggml_tensor * op) {
namespace {
struct ggml_openvino_op_support {
bool is_supported = true;
std::string reason;
operator bool() const {
return is_supported;
}
};
} // namespace
static ggml_openvino_op_support is_op_supported_case(const ggml_tensor * op) {
if (is_msa_block_mask_expansion(op)) {
return true;
return {false, "MSA block mask expansion is not supported"};
}
switch (op->op) {
case GGML_OP_CONCAT: {
if (op->type == GGML_TYPE_I64) {
return true;
return {false, "CONCAT with I64 type is not supported"};
}
if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16 && has_view_op_input(op)) {
return true;
return {false, "CONCAT with BF16 type and VIEW input is not supported on GPU"};
}
break;
}
@@ -1052,24 +1079,21 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// 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;
return {false, "SET op with dst nb1=" + std::to_string(nb1) + ", nb2=" + std::to_string(nb2) + ", nb3=" + std::to_string(nb3) +
" that does not match src0 strides nb[1]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[1]) : "null") +
", nb[2]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[2]) : "null") +
", nb[3]=" + (op->src[0] != nullptr ? std::to_string(op->src[0]->nb[3]) : "null")};
}
break;
}
case GGML_OP_GET_ROWS:
case GGML_OP_SET_ROWS: {
if (op->ne[3] != 1) {
return true;
return {false, "GET_ROWS/SET_ROWS with ne[3] != 1 (ne[3]=" + std::to_string(op->ne[3]) + ") is not supported"};
}
if (op->op == GGML_OP_GET_ROWS && ggml_openvino_get_device_name() == "GPU" &&
op->src[0]->type == GGML_TYPE_BF16) {
return true;
return {false, "GET_ROWS with BF16 src0 is not supported on GPU"};
}
if (op->ne[0] == 256 && (op->src[0]->type == GGML_TYPE_Q4_K || op->src[0]->type == GGML_TYPE_Q5_K ||
op->src[0]->type == GGML_TYPE_Q4_1 || op->src[0]->type == GGML_TYPE_Q5_1)) {
@@ -1078,14 +1102,14 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// 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;
return {false, "GET_ROWS/SET_ROWS with ne[0] == 256 and type " + std::string(ggml_type_name(op->src[0]->type)) +
" rejected due to f16-arithmetic dequant rounding errors that intermittently exceed 1e-7 NMSE threshold"};
}
break;
}
case GGML_OP_RESHAPE: {
if (strncmp(op->name, "ffn_norm_exps", sizeof("ffn_norm_exps") - 1) == 0) {
return true;
return {false, "RESHAPE for ffn_norm_exps is not supported"};
}
break;
}
@@ -1093,11 +1117,13 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
case GGML_OP_MUL:
case GGML_OP_SUB: {
if (op->src[1]->op == GGML_OP_PERMUTE) {
return true;
return {false, "ADD/MUL/SUB with PERMUTE src1 is not supported"};
}
for (int i = 0; i < 4; i++) {
if (op->src[0]->ne[i] != op->src[1]->ne[i] && (op->src[0]->ne[i] != 1 && op->src[1]->ne[i] != 1)) {
return true;
return {false, "ADD/MUL/SUB with incompatible broadcast shapes: src0->ne[" + std::to_string(i) + "]=" +
std::to_string(op->src[0]->ne[i]) + ", src1->ne[" + std::to_string(i) + "]=" +
std::to_string(op->src[1]->ne[i])};
}
}
break;
@@ -1106,7 +1132,7 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// Keep support aligned with the CPU backend implementation, which only handles f32 inputs/output and i32 ids.
if (op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32 || op->src[1]->type != GGML_TYPE_F32 ||
op->src[2]->type != GGML_TYPE_I32) {
return true;
return {false, "ADD_ID only supports F32 inputs/output and I32 ids"};
}
break;
}
@@ -1116,14 +1142,27 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// until the fused GPU kernel is reliable. (falied case llama-arch-test mpt)
if (ggml_openvino_get_device_name() == "GPU" && op->src[1]->ne[0] == op->ne[0] &&
op->src[1]->ne[1] == 1 && op->src[1]->ne[2] == 1 && op->src[1]->ne[3] == 1) {
return true;
return {false, "DIV per-channel scale broadcast is not supported on GPU"};
}
break;
}
case GGML_OP_POOL_2D: {
const auto& name = ggml_openvino_get_device_name();
if (name == "GPU") {
const int32_t * params = op->op_params;
const int k0 = params[1];
const int k1 = params[2];
const int p0 = params[5];
const int p1 = params[6];
if ((p0 > 0 || p1 > 0) && (k0 < 3 || k1 < 3)) {
return {false, "POOL_2D with padding and kernel size < 3 is not supported on " + name};
}
}
break;
}
case GGML_OP_SUM_ROWS: {
// if the input is PERMUTE skip
if (op->src[0]->op == GGML_OP_PERMUTE) {
return true;
return {false, "SUM_ROWS with PERMUTE input is not supported"};
}
break;
}
@@ -1140,54 +1179,51 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// accuracy drift in the OpenVINO path. Restrict by scale=1.0 to avoid
// affecting non-gemma3n models such as Llama-3.2.
if (fabsf(scale - 1.0f) < 1e-6f && is_gemma3n_flash_attn_pattern(op)) {
return true;
return {false, "FLASH_ATTN_EXT gemma3n pattern on GPU is not supported"};
}
if (op->src[4] != nullptr) {
// GGML_LOG_WARN("OpenVINO backend does not support FLASH_ATTN_EXT with sinks\n");
return true;
return {false, "FLASH_ATTN_EXT with sinks is not supported"};
}
if (!is_supported_flash_attn_pattern(op)) {
return true;
return {false, "FLASH_ATTN_EXT unsupported attention pattern"};
}
if (max_bias > 0) {
// GGML_LOG_WARN("OpenVINO backend does not support FLASH_ATTN_EXT with max_bias > 0\n");
return true;
return {false, "FLASH_ATTN_EXT with max_bias > 0 (max_bias=" + std::to_string(max_bias) + ") is not supported"};
}
if (logit_softcap != 0) {
// GGML_LOG_WARN("OpenVINO backend does not support FLASH_ATTN_EXT with logit_softcap != 0\n");
return true;
return {false, "FLASH_ATTN_EXT with logit_softcap != 0 (logit_softcap=" + std::to_string(logit_softcap) + ") is not supported"};
}
break;
}
case GGML_OP_PERMUTE: {
if (op->type == GGML_TYPE_BF16) {
// err msg: [GPU] Could not find a suitable kernel for transpose
// GGML_LOG_WARN("OpenVINO backend does not support PERMUTE with BF16 type\n");
return true;
if (op->type == GGML_TYPE_BF16 && ggml_openvino_get_device_name() == "GPU") {
return {false, "PERMUTE with BF16 type is not supported on GPU"};
}
break;
}
case GGML_OP_CPY: {
if (op->src[0]->type == GGML_TYPE_BF16 || op->src[1]->type == GGML_TYPE_BF16) {
// GGML_LOG_WARN("OpenVINO backend does not support CPY with non-contiguous data or bf16 types\n");
return true;
return {false, "CPY with BF16 src type is not supported"};
}
// CPY to a quantized destination (e.g. f32 -> q4_0) is numerically unstable with OpenVINO backend.
if (ggml_is_quantized(op->type)) {
return true;
return {false, "CPY to quantized destination (e.g. f32 -> q4_0) is numerically unstable"};
}
if (ggml_nelements(op->src[0]) != ggml_nelements(op->src[1])) {
return true;
return {false, "CPY with mismatched element counts is not supported: src0=" + std::to_string(ggml_nelements(op->src[0])) +
" != src1=" + std::to_string(ggml_nelements(op->src[1]))};
}
// op test case with non-contiguous src or dst
if ((op->ne[0] == 3 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) ||
(op->ne[0] == 1 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2) ||
(op->ne[0] == 2 && op->ne[1] == 4 && op->ne[2] == 3 && op->ne[3] == 2)) {
return true;
return {false, "CPY with non-contiguous shape [" + std::to_string(op->ne[0]) + ", " +
std::to_string(op->ne[1]) + ", " + std::to_string(op->ne[2]) + ", " +
std::to_string(op->ne[3]) + "] is not supported"};
}
if (!cpy_output_view_is_supported(op)) {
return true;
return {false, "CPY with non-contiguous output view is not supported"};
}
break;
}
@@ -1196,13 +1232,14 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
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;
return {false, "MUL_MAT quantized benchmark test case on GPU is not supported"};
}
if (op->src[0]->ne[3] != op->src[1]->ne[3] && op->src[0]->ne[3] != 1 && op->src[1]->ne[3] != 1) {
return true;
return {false, "MUL_MAT with incompatible broadcast on ne[3]: src0->ne[3]=" + std::to_string(op->src[0]->ne[3]) +
", src1->ne[3]=" + std::to_string(op->src[1]->ne[3])};
}
if (op->src[0]->op == GGML_OP_VIEW && op->src[1]->op == GGML_OP_VIEW) {
return true;
return {false, "MUL_MAT with both inputs as VIEW is not supported"};
}
break;
}
@@ -1210,16 +1247,17 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// 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;
return {false, "MUL_MAT_ID with single-expert or empty ne[2] <= 1 (ne[2]=" +
std::to_string(op->src[0]->ne[2]) + ") is not supported"};
}
if (ggml_openvino_get_device_name() == "GPU" && op->src[0] != nullptr && op->src[0]->type == GGML_TYPE_BF16) {
return true;
return {false, "MUL_MAT_ID with BF16 weights on GPU is not supported"};
}
// GPU MUL_MAT_ID uses a Gather+MatMul fallback because the GPU plugin rejects internal
// GatherMatmul for these test shapes. Skip cases that would materialize a large selected
// expert-weight temporary.
if (ggml_openvino_get_device_name() == "GPU" && mul_mat_id_requires_large_tmp(op)) {
return true;
return {false, "MUL_MAT_ID requires large temporary on GPU"};
}
break;
}
@@ -1229,51 +1267,46 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
const int mode = op_params[2];
if (op_params[15] != 0) {
// FIXME: support ggml_rope_set_offset
return true;
return {false, "ggml_rope_set_offset is not supported"};
}
if (mode != GGML_ROPE_TYPE_NORMAL && mode != GGML_ROPE_TYPE_NEOX && mode != GGML_ROPE_TYPE_IMROPE) {
// GGML_LOG_WARN("OpenVINO backend does not support ROPE with mode %d\n", mode);
return true;
return {false, "ROPE with mode " + std::to_string(mode) + " is not supported"};
}
const int64_t head_dim = op->src[0]->ne[0];
const int64_t rope_dims = n_dims == 0 ? head_dim : n_dims;
if (rope_dims <= 0 || rope_dims > head_dim || (rope_dims % 2) != 0) {
// 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;
return {false, "ROPE with n_dims=" + std::to_string(n_dims) + ", head_dim=" + std::to_string(head_dim) + " is not supported"};
}
if (op->type != GGML_TYPE_F32 && op->type != GGML_TYPE_F16) {
// GGML_LOG_WARN("OpenVINO backend does not support ROPE with type %s\n", ggml_type_name(op->type));
return true;
return {false, "ROPE with type " + std::string(ggml_type_name(op->type)) + " is not supported"};
}
if (op->src[0]->op == GGML_OP_VIEW) {
if (op->src[0]->view_src->ne[1] != op->src[0]->ne[2]) {
// GGML_LOG_WARN(
// "OpenVINO backend does not support ROPE with src[0]->view_src->ne[1] %ld != src[0]->ne[2] "
// "%ld\n",
// op->src[0]->view_src->ne[1], op->src[0]->ne[2]);
return true;
const struct ggml_tensor * view = op->src[0];
const struct ggml_tensor * view_src = view->view_src;
if (view_src->ne[1] != view->ne[1] || view_src->ne[2] != view->ne[2] || view_src->ne[3] != view->ne[3]) {
return {false, "ROPE with view_src->ne [" + std::to_string(view_src->ne[1]) + ", " +
std::to_string(view_src->ne[2]) + ", " + std::to_string(view_src->ne[3]) +
"] != view->ne [" + std::to_string(view->ne[1]) + ", " +
std::to_string(view->ne[2]) + ", " + std::to_string(view->ne[3]) +
"] is not supported"};
}
}
if (mode == GGML_ROPE_TYPE_IMROPE &&
(op->src[2] != 0 || ((const float *) op_params)[6] != 1 || ((const float *) op_params)[7] != 0 ||
((const float *) op_params)[8] != 1)) {
// GGML_LOG_WARN("OpenVINO backend does not support IMROPE with freq_factors, freq_scale, ext_factor, and attn_factor\n");
return true;
return {false, "IMROPE with freq_factors, freq_scale, ext_factor, and attn_factor is not supported"};
}
break;
}
case GGML_OP_TRANSPOSE: {
// if the type is bf16, will return true
if (op->type == GGML_TYPE_BF16) {
// GGML_LOG_WARN("OpenVINO backend does not support CONT with BF16 type\n");
return true;
return {false, "TRANSPOSE with BF16 type is not supported"};
}
break;
}
case GGML_OP_REPEAT: {
if (ggml_openvino_get_device_name() == "GPU" && op->type == GGML_TYPE_BF16) {
return true;
return {false, "REPEAT with BF16 type is not supported on GPU"};
}
break;
}
@@ -1285,15 +1318,15 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// return true;
// }
if (op->src[2]->op == GGML_OP_PERMUTE) {
return true;
return {false, "GATED_DELTA_NET with PERMUTE src2 is not supported"};
}
// kda (per-key-dimension gating) not supported by fused GatedDeltaNet op
if (op->src[3]->ne[0] != 1) {
return true;
return {false, "GATED_DELTA_NET with kda (per-key-dimension gating) is not supported"};
}
// K > 1 (multiple state snapshots) not supported by fused op
if (((const int32_t *) op->op_params)[0] > 1) {
return true;
return {false, "GATED_DELTA_NET with K > 1 (multiple state snapshots) is not supported"};
}
break;
}
@@ -1307,17 +1340,17 @@ static bool is_op_unsupported_case(const ggml_tensor * op) {
// 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;
return {false, "VIEW for selected_experts (argsort_top_k) is not supported"};
}
break;
}
default:
break;
}
return false;
return {true, ""};
}
static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, const ggml_tensor * op) {
static ggml_openvino_op_support ggml_backend_openvino_device_supports_op_impl(ggml_backend_dev_t dev, const ggml_tensor * op) {
GGML_ASSERT(dev->reg != nullptr);
static std::unordered_set<ggml_type> supported_types{
@@ -1367,48 +1400,41 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
case GGML_OP_UNARY: {
auto supported = supported_unary_ops.find(ggml_get_unary_op(op)) != supported_unary_ops.end();
if (!supported) {
// GGML_LOG_WARN("OpenVINO backend does not support unary op %s\n", ggml_unary_op_name(ggml_get_unary_op(op)));
return false;
return {false, "unary op " + std::string(ggml_unary_op_name(ggml_get_unary_op(op))) + " has no op translator"};
}
if (ggml_get_unary_op(op) == GGML_UNARY_OP_EXP && op->type == GGML_TYPE_F32) {
return false;
return {false, "UNARY_EXP with F32 type is not supported"};
}
break;
}
case GGML_OP_GLU: {
auto supported = supported_glu_ops.find(ggml_get_glu_op(op)) != supported_glu_ops.end();
if (!supported) {
// GGML_LOG_WARN("OpenVINO backend does not support GLU op %s\n", ggml_glu_op_name(ggml_get_glu_op(op)));
return false;
return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " has no op translator"};
}
// if (has_view_op_input(op)) {
// // 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;
// return {false, "GLU op " + std::string(ggml_glu_op_name(ggml_get_glu_op(op))) + " with view input is not supported"};
// }
if (op->src[1] == nullptr && op->src[0]->ne[0] % 2 != 0) {
// triggers bug in ov gpu
return false;
return {false, "GLU op with odd src0 ne[0] and null src1 is not supported"};
}
break;
}
default: {
auto supported = supported_ops.find(op->op) != supported_ops.end();
if (!supported) {
// GGML_LOG_WARN("OpenVINO backend does not support op %s\n", ggml_op_name(op->op));
return false;
return {false, "op " + std::string(ggml_op_name(op->op)) + " has no op translator"};
}
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;
return {false, "op " + std::string(ggml_op_name(op->op)) + " with VIEW input is not supported"};
}
}
}
if (supported_types.find(op->type) == supported_types.end()) {
// GGML_LOG_WARN("OpenVINO backend does not support tensor type %s\n", ggml_type_name(op->type));
return false;
return {false, "tensor type " + std::string(ggml_type_name(op->type)) + " is not supported"};
}
for (int i = 0; i < GGML_MAX_SRC; i++) {
auto * src = op->src[i];
@@ -1416,21 +1442,32 @@ static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, con
break;
}
if (supported_types.find(src->type) == supported_types.end()) {
// GGML_LOG_WARN("OpenVINO backend does not support tensor type %s\n", ggml_type_name(src->type));
return false;
return {false, "src[" + std::to_string(i) + "] type " + std::string(ggml_type_name(src->type)) + " is not supported"};
}
const bool is_supported_3d_moe_expert =
op->op == GGML_OP_MUL_MAT_ID && i == 0 && (src->type == GGML_TYPE_MXFP4 || src->ne[3] == 1);
if (ggml_is_quantized(src->type) && src->ne[2] != 1 && !is_supported_3d_moe_expert) {
// GGML_LOG_WARN("OpenVINO backend does not support 3D quantized tensors\n");
return false;
return {false, "3D quantized tensor for src[" + std::to_string(i) + "] is not supported"};
}
}
if (is_op_unsupported_case(op)) {
return false;
auto op_support_case = is_op_supported_case(op);
if (!op_support_case.is_supported) {
return op_support_case;
}
return true;
return {true, ""};
}
static bool ggml_backend_openvino_device_supports_op(ggml_backend_dev_t dev, const ggml_tensor * op) {
auto res = ggml_backend_openvino_device_supports_op_impl(dev, op);
if (!res.is_supported) {
static const bool log_unsupported = ggml_openvino_getenv_int("GGML_OPENVINO_LOG_UNSUPPORTED_OPS") != 0;
if (log_unsupported) {
GGML_LOG_WARN("OpenVINO op unsupported: op '%s' (%s), type %s: %s\n",
op->name, ggml_op_name(op->op), ggml_type_name(op->type), res.reason.c_str());
}
}
return res.is_supported;
}
static bool ggml_backend_openvino_device_supports_buft(ggml_backend_dev_t dev, ggml_backend_buffer_type_t buft) {
+126 -14
View File
@@ -3,8 +3,11 @@
#include "../utils.h"
#include <climits>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <vector>
#include <numeric>
#include <openvino/frontend/exception.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
@@ -12,9 +15,14 @@
#include <openvino/op/gather.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/range.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scatter_update.hpp>
#include <openvino/op/shape_of.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/squeeze.hpp>
#include <openvino/op/subtract.hpp>
#include <vector>
namespace ov {
namespace frontend {
@@ -61,10 +69,27 @@ OutputVector translate_cpy(const NodeContext & context) {
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.
// Recurrent state cache writeback into a slot block of the cache. Where the block starts is a
// runtime input, so the cached model works for any kv head and active sequence 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.
if (op_case == 3) {
// With -np 1 (and generally whenever there is no defrag remainder) this GET_ROWS gathers
// zero rows: nothing to write back, and the cache is unchanged. NPU rejects zero-size
// tensors, so short-circuit instead of building a degenerate Slice/Concat chain.
bool is_empty = false;
if (input_shape.rank().is_static()) {
for (const auto & d : input_shape) {
if (d.is_static() && d.get_length() == 0) {
is_empty = true;
break;
}
}
}
if (is_empty) {
return {context.get_input(1)};
}
}
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);
@@ -81,19 +106,49 @@ OutputVector translate_cpy(const NodeContext & context) {
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);
ov::Output<ov::Node> state_begin;
const std::string src_begin_name = "rs_src_begin_" + context.get_name();
if (context.has_input(src_begin_name)) {
state_begin = context.get_input(src_begin_name);
} else {
auto ssm_state_size = context.get_ssm_state_size();
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_begin = std::make_shared<ov::op::v0::Negative>(state_rows);
} else {
state_begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {-ssm_state_size});
}
}
auto state_part =
std::make_shared<ov::op::v8::Slice>(context.get_input(0), state_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.
// conv_input is [previous conv state | new tokens]; the snapshot is the conv_kernel_size - 1
// columns ending at the last *valid* token. Gather (rather than Slice) keeps the output
// shape static even though the window start is a runtime value.
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}));
ov::Output<ov::Node> window;
auto col_axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
const std::string src_begin_name = "rs_src_begin_" + context.get_name();
if (context.has_input(src_begin_name)) {
auto src_begin = context.get_input(src_begin_name);
auto src_end = std::make_shared<ov::op::v1::Add>(
src_begin, ov::op::v0::Constant::create(ov::element::i64, {1}, {window_size}));
window = std::make_shared<ov::op::v8::Slice>(context.get_input(0), src_begin, src_end, one, col_axis);
} else if (context.has_input("chunk_valid_len")) {
std::vector<int64_t> offsets(window_size);
std::iota(offsets.begin(), offsets.end(), 0);
auto indices = std::make_shared<ov::op::v1::Add>(
ov::op::v0::Constant::create(ov::element::i64, {(size_t) window_size}, offsets),
context.get_input("chunk_valid_len"));
window = std::make_shared<ov::op::v8::Gather>(context.get_input(0), indices, col_axis);
} else {
auto window_begin = ov::op::v0::Constant::create(ov::element::i64, {1}, {-window_size});
window =
std::make_shared<ov::op::v8::Slice>(context.get_input(0), window_begin, int_max, one, col_axis);
}
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");
@@ -157,6 +212,63 @@ OutputVector translate_cpy(const NodeContext & context) {
auto input = process_view_input_new(context, 0);
if (op_case == 5 || op_case == 6) {
auto input_shape = context.get_input_shape(0);
auto output_shape = context.get_output_shape();
auto dst_ggml_shape = context.get_view_input_ggml_shape(1, 0);
auto dst_stride = context.get_view_input_stride(1, 0);
size_t offset_bytes = context.get_view_input_offset(1, 0);
auto n_state = (int64_t) context.get_input_shape(0)[3].get_length();
auto n_state_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_state});
auto kv_buf = context.get_input(1); // shape {1,1,1,N}
Output<Node> token_len_per_seq;
Output<Node> n_write_dyn;
if (context.has_input("token_len_per_seq")) {
token_len_per_seq = context.get_input("token_len_per_seq");
n_write_dyn = std::make_shared<ov::op::v1::Multiply>(token_len_per_seq, n_state_c);
} else {
n_write_dyn = ov::op::v0::Constant::create(ov::element::i64, {1}, {(int64_t) dst_ggml_shape[3]});
}
size_t elem_size = dst_stride[3];
FRONT_END_OP_CONVERSION_CHECK(elem_size > 0, "CPY KV cache view update has invalid element size");
int64_t start_elem = (int64_t) (offset_bytes / elem_size);
// op_case 5: decoder self-attention write offset advances each step.
// op_case 6: encoder self-attn or cross-attn offset fixed at compile time.
const bool is_decoder_self_attn = (op_case == 5);
auto ones_c = ov::op::v0::Constant::create(ov::element::i64, {3}, std::vector<int64_t>{1, 1, 1});
auto new_shape = std::make_shared<ov::op::v0::Concat>(ov::OutputVector{ones_c, n_write_dyn}, 0);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(input, new_shape, false);
auto data = std::make_shared<ov::op::v0::Convert>(reshaped, context.get_output_type());
// Indices [start_elem .. start_elem + n_write) on axis 3 of {1,1,1,N}
// For decoder self-attention the write offset advances each step, so compute it
// dynamically from the model inputs: start = (attention_size - token_len_per_seq) * n_state.
// For encoder self-attn and cross-attn the offset is fixed at graph-compile time.
ov::Output<ov::Node> start;
if (is_decoder_self_attn && context.has_input("attention_size") && context.has_input("token_len_per_seq")) {
auto attention_size_in = context.get_input("attention_size");
auto token_len_in = context.get_input("token_len_per_seq");
auto past_tokens = std::make_shared<ov::op::v1::Subtract>(attention_size_in, token_len_in);
auto new_start = std::make_shared<ov::op::v1::Multiply>(past_tokens, n_state_c);
start = std::make_shared<ov::op::v1::Add>(
new_start, ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem}));
} else {
start = ov::op::v0::Constant::create(ov::element::i64, {1}, {start_elem});
}
auto start_squeezed = std::make_shared<ov::op::v0::Squeeze>(start);
auto end = std::make_shared<ov::op::v1::Add>(start_squeezed, n_write_dyn);
auto end_squeezed = std::make_shared<ov::op::v0::Squeeze>(end);
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto step_squeezed = std::make_shared<ov::op::v0::Squeeze>(step);
auto indices =
std::make_shared<ov::op::v4::Range>(start_squeezed, end_squeezed, step_squeezed, ov::element::i64);
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto kv_updated = std::make_shared<ov::op::v3::ScatterUpdate>(kv_buf, indices, data, axis);
return rename_outputs_with_suffix({kv_updated}, context.get_name());
}
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());
@@ -3,8 +3,8 @@
#include "../utils.h"
#include "ggml-openvino/ggml-openvino-extra.h"
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
@@ -15,6 +15,7 @@
#include <openvino/op/multiply.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/scaled_dot_product_attention.hpp>
#include <openvino/op/slice.hpp>
#include <openvino/op/softmax.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/op/unsqueeze.hpp>
@@ -24,13 +25,62 @@ namespace ov {
namespace frontend {
namespace ggml {
namespace op {
static ov::Output<ov::Node> reshape_flat_kv(const ov::Output<ov::Node> & kv_flat,
size_t view_offset_bytes,
size_t nb1_bytes,
int64_t n_head,
int64_t head_size,
const ov::Output<ov::Node> & attention_size) {
int64_t n_state = n_head * head_size;
int64_t layer_start_elem = (int64_t) (view_offset_bytes / (nb1_bytes / n_state));
// Dynamic slice: [layer_start_elem, layer_start_elem + n_kv * n_state)
auto start_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {layer_start_elem});
auto n_state_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_state});
// end = start + attention_size * n_state (both static + dynamic)
auto kv_len_elems = std::make_shared<ov::op::v1::Multiply>(attention_size, n_state_c);
auto end_c = std::make_shared<ov::op::v1::Add>(start_c, kv_len_elems);
auto step_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto axis_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {3});
auto sliced = std::make_shared<ov::op::v8::Slice>(kv_flat, start_c, end_c, step_c, axis_c);
// KV cache is laid out as {n_kv, n_head, head_size} in memory
// Reshape to {1, n_kv, n_head, head_size}, then transpose to {1, n_head, n_kv, head_size}
// as required by SDPA.
auto one_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto n_head_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {n_head});
auto head_size_c = ov::op::v0::Constant::create(ov::element::i64, {1}, {head_size});
// reshape: {n_kv*n_state} -> {1, n_kv, n_head, head_size}
auto new_shape =
std::make_shared<ov::op::v0::Concat>(ov::OutputVector{one_c, attention_size, n_head_c, head_size_c}, 0);
auto reshaped = std::make_shared<ov::op::v1::Reshape>(sliced, new_shape, false);
// transpose: {1, n_kv, n_head, head_size} -> {1, n_head, n_kv, head_size}
auto perm = ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3});
auto ret = std::make_shared<ov::op::v1::Transpose>(reshaped, perm);
return ret;
}
OutputVector translate_flash_attn_ext(const NodeContext & context) {
num_inputs_check(context, 4, 4);
num_inputs_check(context, 3, 4);
const bool has_mask = context.get_input_size() == 4;
auto q_f32 = context.get_input(0);
auto k = context.get_input(1);
auto v = context.get_input(2);
auto mask = context.get_input(3);
const int op_case = context.get_op_case();
if (op_case == 1 || op_case == 2) {
int64_t n_state_head = (int64_t) context.get_view_input_ggml_shape(1, 0)[3];
int64_t n_head = (int64_t) context.get_view_input_ggml_shape(1, 0)[1];
size_t nb1 = context.get_view_input_stride(1, 0)[2];
size_t offset = context.get_view_input_offset(1, 0);
ov::Output<ov::Node> attention_size;
if (op_case == 1) {
attention_size = context.get_input("attention_size");
} else {
attention_size = context.get_input("attention_size_static");
}
k = reshape_flat_kv(k, offset, nb1, n_head, n_state_head, attention_size);
v = reshape_flat_kv(v, offset, nb1, n_head, n_state_head, attention_size);
}
float * params = reinterpret_cast<float *>(context.get_output_op_params());
float scale = params[0];
@@ -43,16 +93,19 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
ov::Output<ov::Node> res;
// For stateful
std::string mask_name = "KQ_mask_sliced";
if (context.get_input_names()[3].find("swa") != std::string::npos) {
mask_name = "KQ_mask_swa_sliced";
}
if (context.has_input(mask_name)) {
mask = context.get_input(mask_name);
}
if (mask.get_element_type() != ov::element::f16) {
mask = std::make_shared<ov::op::v0::Convert>(mask, ov::element::f16);
ov::Output<ov::Node> mask;
if (has_mask) {
mask = context.get_input(3);
std::string mask_name = "KQ_mask_sliced";
if (context.get_input_names()[3].find("swa") != std::string::npos) {
mask_name = "KQ_mask_swa_sliced";
}
if (context.has_input(mask_name)) {
mask = context.get_input(mask_name);
}
if (mask.get_element_type() != ov::element::f16) {
mask = std::make_shared<ov::op::v0::Convert>(mask, ov::element::f16);
}
}
//auto tile_kv = [&](int64_t num_heads, int64_t num_heads_kv, int64_t head_size, ov::Output<Node> kv) {
@@ -108,10 +161,14 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
// get [B, 1, 1, S_q, S_k], which NUMPY-broadcasts cleanly against the
// [B, num_heads_kv, factor, S_q, S_k] scores: B==B, then 1→num_heads_kv and
// 1→factor on the head dims.
auto mask_unsq1 =
std::make_shared<ov::op::v0::Unsqueeze>(mask, ov::op::v0::Constant::create(ov::element::i64, {1}, {2}));
// mask_unsq1: [B, 1, 1, S_q, S_k] (rank 5)
ov::Output<ov::Node> qk_masked = std::make_shared<ov::op::v1::Add>(qk_scaled, mask_unsq1);
ov::Output<ov::Node> qk_masked;
if (has_mask) {
auto mask_unsq1 =
std::make_shared<ov::op::v0::Unsqueeze>(mask, ov::op::v0::Constant::create(ov::element::i64, {1}, {2}));
qk_masked = std::make_shared<ov::op::v1::Add>(qk_scaled, mask_unsq1);
} else {
qk_masked = qk_scaled;
}
auto softmax = std::make_shared<ov::op::v8::Softmax>(qk_masked, /*axis=*/-1);
@@ -164,9 +221,16 @@ OutputVector translate_flash_attn_ext(const NodeContext & context) {
k = tile_kv(num_heads, num_heads_kv, head_size, k);
v = tile_kv(num_heads, num_heads_kv, head_size, v);
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, mask, scale_node, false);
res = std::make_shared<ov::op::v1::Transpose>(sdpa,
ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
constexpr auto causal = false;
if (has_mask) {
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, mask, scale_node, causal);
res = std::make_shared<ov::op::v1::Transpose>(
sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
} else {
auto sdpa = std::make_shared<ov::op::v13::ScaledDotProductAttention>(q, k, v, scale_node, causal);
res = std::make_shared<ov::op::v1::Transpose>(
sdpa, ov::op::v0::Constant::create(ov::element::i64, {4}, {0, 2, 1, 3}));
}
res = std::make_shared<ov::op::v0::Convert>(res, ov::element::f32);
return rename_outputs_with_suffix({res}, context.get_name());
}
@@ -7,12 +7,15 @@
#include <cmath>
#include <cstdint>
#include <memory>
#include <numeric>
#include <openvino/op/add.hpp>
#include <openvino/op/broadcast.hpp>
#include <openvino/op/concat.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/exp.hpp>
#include <openvino/op/gather.hpp>
#include <openvino/op/less.hpp>
#include <openvino/op/loop.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
@@ -80,6 +83,28 @@ OutputVector translate_gated_delta_net(const NodeContext & context) {
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}));
if (context.has_input("chunk_valid_len")) {
// The last prefill chunk is padded with fabricated tokens. The recurrence is
// S_t = S_{t-1} * exp(g_t) + k_t (x) ((v_t - S_{t-1}^T k_t) * beta_t)
// so forcing g = 0 and beta = 0 makes a padded step an exact identity and keeps the final
// state equal to the state after the last real token. Attention output at those positions
// is garbage but never read.
const auto & g_ps = g.get_partial_shape();
FRONT_END_OP_CONVERSION_CHECK(g_ps.rank().is_static() && g_ps.rank().get_length() == 3 && g_ps[1].is_static(),
"GATED_DELTA_NET pad masking requires a static token dimension");
const int64_t n_tokens = g_ps[1].get_length();
std::vector<int64_t> positions(n_tokens);
std::iota(positions.begin(), positions.end(), 0);
auto valid = std::make_shared<ov::op::v1::Less>(
ov::op::v0::Constant::create(ov::element::i64, {(size_t) n_tokens}, positions),
context.get_input("chunk_valid_len"));
auto mask = std::make_shared<ov::op::v0::Unsqueeze>(
std::make_shared<ov::op::v0::Convert>(valid, g.get_element_type()),
ov::op::v0::Constant::create(ov::element::i64, {2}, std::vector<int64_t>{0, 2}));
g = std::make_shared<ov::op::v1::Multiply>(g, mask);
beta = std::make_shared<ov::op::v1::Multiply>(beta, mask);
}
// 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;
@@ -0,0 +1,64 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <memory>
#include <openvino/core/node_output.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/slice.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_glu_geglu_quick(const NodeContext & context) {
num_inputs_check(context, 1, 2);
ov::Output<ov::Node> src0;
ov::Output<ov::Node> src1;
if (context.get_input_size() == 2) {
src0 = process_view_input_new(context, 0);
src1 = process_view_input_new(context, 1);
} else {
// split along last axis, nc = ne[0] / 2
auto combined = process_view_input_new(context, 0);
auto combined_shape = combined.get_partial_shape();
int64_t last_dim_val = combined_shape[combined_shape.rank().get_length() - 1].get_length();
int64_t nc = last_dim_val / 2;
auto axis = ov::op::v0::Constant::create(ov::element::i64, {1}, {-1});
auto step = ov::op::v0::Constant::create(ov::element::i64, {1}, {1});
auto start0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {0});
auto stop0 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto start1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {nc});
auto stop1 = ov::op::v0::Constant::create(ov::element::i64, {1}, {2 * nc});
src0 = std::make_shared<ov::op::v8::Slice>(combined, start0, stop0, step, axis);
src1 = std::make_shared<ov::op::v8::Slice>(combined, start1, stop1, step, axis);
}
int32_t * params = context.get_output_op_params();
const int32_t swapped = params[1];
if (swapped) {
std::swap(src0, src1);
}
// GELU_QUICK(x) = x * sigmoid(1.702 * x)
// Create the constant in the same type as src0 to avoid f16/f32 mismatch.
auto input_type = src0.get_element_type();
auto coef = ov::op::v0::Constant::create(input_type, ov::Shape{}, {1.702f});
auto scaled = std::make_shared<ov::op::v1::Multiply>(src0, coef);
auto sigmoid = std::make_shared<ov::op::v0::Sigmoid>(scaled);
auto gated = std::make_shared<ov::op::v1::Multiply>(src0, sigmoid);
auto res = std::make_shared<ov::op::v1::Multiply>(gated, src1);
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,53 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/avg_pool.hpp>
#include <openvino/op/max_pool.hpp>
#include <openvino/op/convert.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_pool_2d(const NodeContext & context) {
num_inputs_check(context, 1, 1);
const int32_t * params = context.get_output_op_params();
const int k0 = params[1];
const int k1 = params[2];
const int s0 = params[3];
const int s1 = params[4];
const int p0 = params[5];
const int p1 = params[6];
const int op_case = context.get_op_case();
ov::Output<Node> input = context.get_input(0);
ov::Strides strides{static_cast<size_t>(s1), static_cast<size_t>(s0)};
ov::Shape pads_begin{static_cast<size_t>(p1), static_cast<size_t>(p0)};
ov::Shape pads_end{static_cast<size_t>(p1), static_cast<size_t>(p0)};
ov::Shape kernel{static_cast<size_t>(k1), static_cast<size_t>(k0)};
ov::Output<Node> res;
switch (op_case) {
case 1: // GGML_OP_POOL_MAX
{
res = std::make_shared<ov::op::v1::MaxPool>(input, strides, pads_begin, pads_end, kernel);
break;
}
case 2: // GGML_OP_POOL_AVG
{
res = std::make_shared<ov::op::v1::AvgPool>(input, strides, pads_begin, pads_end, kernel, false);
break;
}
default:
break;
}
return rename_outputs_with_suffix({res}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,36 @@
#include "../node_context.h"
#include "../op_table.h"
#include "../utils.h"
#include <openvino/op/roll.hpp>
#include <openvino/op/constant.hpp>
namespace ov {
namespace frontend {
namespace ggml {
namespace op {
OutputVector translate_roll(const NodeContext & context) {
num_inputs_check(context, 1, 1);
const int32_t * params = context.get_output_op_params();
int64_t s0 = params[0];
int64_t s1 = params[1];
int64_t s2 = params[2];
int64_t s3 = params[3];
auto input = context.get_input(0);
auto shift = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{s3, s2, s1, s0});
auto axes = ov::op::v0::Constant::create(
ov::element::i64, ov::Shape{4}, std::vector<int64_t>{0, 1, 2, 3});
auto roll = std::make_shared<ov::op::v7::Roll>(input, shift, axes);
return rename_outputs_with_suffix({roll}, context.get_name());
}
} // namespace op
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -17,6 +17,13 @@ namespace op {
OutputVector translate_view(const NodeContext & context) {
num_inputs_check(context, 1, 1);
if (context.get_op_case() == 1) {
// Static-mode identity pass-through for VIEWs over a GATED_DELTA_NET combined output or
// the conv_input CONCAT; the consuming op (CPY/RMS_NORM) does its own runtime-correct
// slicing on the full tensor (see ggml-decoder.cpp compute_op_case, GGML_OP_VIEW).
return {context.get_input(0)};
}
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
@@ -10,6 +10,7 @@
#include <openvino/op/matmul.hpp>
#include <openvino/op/multiply.hpp>
#include <openvino/op/negative.hpp>
#include <openvino/op/relu.hpp>
#include <openvino/op/sigmoid.hpp>
#include <openvino/op/subtract.hpp>
#include <openvino/op/tanh.hpp>
@@ -55,10 +56,12 @@ std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
{"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_UNARY_OP_RELU", op::translate_1to1_match_1_input<v0::Relu> },
{"GGML_OP_VIEW", op::translate_view },
{"GGML_GLU_OP_SWIGLU", op::translate_glu_swiglu },
{"GGML_GLU_OP_SWIGLU_OAI", op::translate_glu_swiglu_oai },
{"GGML_GLU_OP_GEGLU", op::translate_glu_geglu },
{"GGML_GLU_OP_GEGLU_QUICK", op::translate_glu_geglu_quick },
{"GGML_OP_SET_ROWS", op::translate_set_rows },
{"GGML_OP_CPY", op::translate_cpy },
{"GGML_OP_FLASH_ATTN_EXT", op::translate_flash_attn_ext },
@@ -72,6 +75,8 @@ std::unordered_map<std::string, CreatorFunction> get_supported_ops() {
{"GGML_OP_DIAG", op::translate_diag },
{"GGML_OP_TRI", op::translate_tri },
{"GGML_OP_SET", op::translate_set },
{"GGML_OP_POOL_2D", op::translate_pool_2d },
{"GGML_OP_ROLL", op::translate_roll },
// solve_tri has accuracy issues on GPU
// {"GGML_OP_SOLVE_TRI", op::translate_solve_tri },
};
@@ -38,6 +38,7 @@ GGML_OP_CONVERTER(translate_view);
GGML_OP_CONVERTER(translate_glu_swiglu);
GGML_OP_CONVERTER(translate_glu_swiglu_oai);
GGML_OP_CONVERTER(translate_glu_geglu);
GGML_OP_CONVERTER(translate_glu_geglu_quick);
GGML_OP_CONVERTER(translate_set_rows);
GGML_OP_CONVERTER(translate_cpy);
GGML_OP_CONVERTER(translate_argsort);
@@ -53,6 +54,8 @@ GGML_OP_CONVERTER(translate_set);
GGML_OP_CONVERTER(translate_diag);
GGML_OP_CONVERTER(translate_tri);
GGML_OP_CONVERTER(translate_solve_tri);
GGML_OP_CONVERTER(translate_pool_2d);
GGML_OP_CONVERTER(translate_roll);
} // namespace op
@@ -0,0 +1,212 @@
#include "fuse_to_conv.h"
#include <openvino/core/graph_util.hpp>
#include <openvino/core/rt_info.hpp>
#include <openvino/op/add.hpp>
#include <openvino/op/constant.hpp>
#include <openvino/op/convert.hpp>
#include <openvino/op/convolution.hpp>
#include <openvino/op/extractimagepatches.hpp>
#include <openvino/op/matmul.hpp>
#include <openvino/op/pad.hpp>
#include <openvino/op/reshape.hpp>
#include <openvino/op/transpose.hpp>
#include <openvino/pass/pattern/op/label.hpp>
#include <openvino/pass/pattern/op/pattern.hpp>
#include <openvino/pass/pattern/op/wrap_type.hpp>
namespace opp = ov::pass::pattern;
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
// This pass fuses an IM2COL + MatMul convolution into OpenVINO's Convolution op for performance gains.
// Reference the im2col.cpp translator for reference on the pattern being matched.
FuseToConv::FuseToConv() {
const auto m_wei = opp::any_input();
const auto m_act = opp::any_input();
const auto m_matmul = opp::wrap_type<ov::op::v0::MatMul>({m_wei, m_act});
const auto callback = [=](ov::pass::pattern::Matcher & m) {
const auto & pm = m.get_pattern_value_map();
auto matmul_node = ov::as_type_ptr<ov::op::v0::MatMul>(pm.at(m_matmul).get_node_shared_ptr());
if (!matmul_node || matmul_node->get_transpose_a() || !matmul_node->get_transpose_b()) {
return false;
}
auto trace = matmul_node->input_value(1);
// Optional Convert
if (auto n = ov::as_type_ptr<ov::op::v0::Convert>(trace.get_node_shared_ptr())) {
trace = n->input_value(0);
}
for (int i = 0; i < 2; ++i) {
auto n = ov::as_type_ptr<ov::op::v1::Reshape>(trace.get_node_shared_ptr());
if (!n) {
return false;
}
trace = n->input_value(0);
}
if (auto n = ov::as_type_ptr<ov::op::v1::Transpose>(trace.get_node_shared_ptr())) {
trace = n->input_value(0);
} else {
return false;
}
if (auto n = ov::as_type_ptr<ov::op::v1::Reshape>(trace.get_node_shared_ptr())) {
trace = n->input_value(0);
} else {
return false;
}
if (auto n = ov::as_type_ptr<ov::op::v1::Transpose>(trace.get_node_shared_ptr())) {
trace = n->input_value(0);
} else {
return false;
}
auto eip = ov::as_type_ptr<ov::op::v3::ExtractImagePatches>(trace.get_node_shared_ptr());
if (!eip) {
return false;
}
const auto eip_strides = eip->get_strides(); // {stride_h, stride_w}
const auto eip_rates = eip->get_rates(); // {dil_h, dil_w}
auto pad = ov::as_type_ptr<ov::op::v1::Pad>(eip->input_value(0).get_node_shared_ptr());
if (!pad) {
return false;
}
auto pads_begin_const =
ov::as_type_ptr<ov::op::v0::Constant>(pad->input_value(1).get_node_shared_ptr());
const auto pads_begin_vals = pads_begin_const->cast_vector<int64_t>(); // {0, 0, pad_h, pad_w}
const std::ptrdiff_t pad_h = static_cast<std::ptrdiff_t>(pads_begin_vals[2]);
const std::ptrdiff_t pad_w = static_cast<std::ptrdiff_t>(pads_begin_vals[3]);
auto image_input = pad->input_value(0); // [N, IC, 1, IW] NCHW
auto w_trace = matmul_node->input_value(0);
if (auto n = ov::as_type_ptr<ov::op::v0::Convert>(w_trace.get_node_shared_ptr())) {
w_trace = n->input_value(0);
}
for (int i = 0; i < 2; ++i) {
auto n = ov::as_type_ptr<ov::op::v1::Reshape>(w_trace.get_node_shared_ptr());
if (!n) {
break;
}
w_trace = n->input_value(0);
}
auto weight_const = ov::as_type_ptr<ov::op::v0::Constant>(w_trace.get_node_shared_ptr());
if (!weight_const) {
return false;
}
// Reshape weight to [OC, IC, 1, KW] (OIHW).
const auto w_shape = weight_const->get_shape();
ov::Shape conv_w_shape;
if (w_shape.size() == 3) {
conv_w_shape = {w_shape[0], w_shape[1], 1, w_shape[2]};
} else if (w_shape.size() == 4) {
conv_w_shape = {w_shape[1], w_shape[2], 1, w_shape[3]};
} else {
return false;
}
auto weight_reshaped = register_new_node<ov::op::v0::Constant>(weight_const->get_element_type(), conv_w_shape,
weight_const->get_data_ptr());
ov::Output<Node> weight_input = weight_reshaped;
if (weight_reshaped->get_element_type() != image_input.get_element_type()) {
weight_input = register_new_node<ov::op::v0::Convert>(weight_reshaped, image_input.get_element_type());
}
auto conv = register_new_node<ov::op::v1::Convolution>(
image_input, weight_input,
ov::Strides{static_cast<size_t>(eip_strides[0]), static_cast<size_t>(eip_strides[1])},
ov::CoordinateDiff{pad_h, pad_w}, ov::CoordinateDiff{pad_h, pad_w},
ov::Strides{static_cast<size_t>(eip_rates[0]), static_cast<size_t>(eip_rates[1])},
ov::op::PadType::EXPLICIT);
constexpr auto target_type = ov::element::f32;
ov::Output<Node> conv_out = conv;
if (conv_out.get_element_type() != target_type) {
conv_out = register_new_node<ov::op::v0::Convert>(conv_out, target_type);
}
std::shared_ptr<ov::op::v1::Add> add_node;
ov::Output<Node> bias_input;
for (const auto & consumer_in : matmul_node->output(0).get_target_inputs()) {
auto cast = ov::as_type_ptr<ov::op::v0::Convert>(consumer_in.get_node()->shared_from_this());
if (!cast) {
continue;
}
for (const auto & add_in : cast->output(0).get_target_inputs()) {
auto add = ov::as_type_ptr<ov::op::v1::Add>(add_in.get_node()->shared_from_this());
if (!add) {
continue;
}
for (size_t i = 0; i < 2; ++i) {
if (ov::as_type_ptr<ov::op::v0::Constant>(add->input_value(i).get_node_shared_ptr())) {
bias_input = add->input_value(i);
add_node = add;
break;
}
}
if (add_node) {
break;
}
}
if (add_node) {
break;
}
}
ov::Output<Node> final_out;
std::shared_ptr<Node> target_node;
if (add_node) {
// Reshape bias [OC, 1] → [1, OC, 1, 1] for NCHW broadcasting.
ov::Output<Node> bias = bias_input;
if (bias.get_element_type() != target_type) {
bias = register_new_node<ov::op::v0::Convert>(bias, target_type);
}
const auto oc = static_cast<int64_t>(conv_w_shape[0]);
auto bias_shape = register_new_node<ov::op::v0::Constant>(ov::element::i64, ov::Shape{4},
std::vector<int64_t>{1, oc, 1, 1});
bias = register_new_node<ov::op::v1::Reshape>(bias, bias_shape, false);
final_out = register_new_node<ov::op::v1::Add>(conv_out, bias);
target_node = add_node;
} else {
final_out = conv_out;
target_node = matmul_node;
}
// Reshape final output back to the target node's original shape if needed.
auto orig_shape = target_node->get_output_partial_shape(0);
if (orig_shape.is_static() && final_out.get_partial_shape() != orig_shape) {
auto shape_const = register_new_node<ov::op::v0::Constant>(ov::element::i64, ov::Shape{orig_shape.size()},
orig_shape.to_shape());
final_out = register_new_node<ov::op::v1::Reshape>(final_out, shape_const, false);
}
final_out.get_node_shared_ptr()->set_friendly_name(target_node->get_friendly_name());
ov::copy_runtime_info(m.get_matched_nodes(), final_out.get_node_shared_ptr());
ov::replace_node(target_node, final_out.get_node_shared_ptr());
return true;
};
register_matcher(std::make_shared<opp::Matcher>(m_matmul, "ov::frontend::ggml::pass::FuseToConv"), callback);
}
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -0,0 +1,17 @@
#include "openvino/pass/matcher_pass.hpp"
namespace ov {
namespace frontend {
namespace ggml {
namespace pass {
class FuseToConv : public ov::pass::MatcherPass {
public:
OPENVINO_MATCHER_PASS_RTTI("ov::frontend::ggml::pass::FuseToConv")
FuseToConv();
};
} // namespace pass
} // namespace ggml
} // namespace frontend
} // namespace ov
@@ -5,6 +5,7 @@
#include "ggml-openvino/openvino/node_context.h"
#include "ggml-openvino/openvino/utils.h"
#include "input_model.h"
#include "pass/fuse_to_conv.h"
#include "pass/mark_decompression_convert_constant_folding.h"
#include "pass/mark_dequantization_subgraph.h"
#include "pass/squeeze_matmul.h"
@@ -109,7 +110,8 @@ ov::pass::MakeStateful::ParamResPairs get_kv_param_res_pairs(
void add_sliced_mask_stateful(TensorMap & tensor_map) {
auto create_sliced_mask = [&](const std::string & mask_name, const std::string & sliced_name) {
if ((tensor_map.find(mask_name) != tensor_map.end()) &&
(tensor_map.find("token_len_per_seq") != tensor_map.end())) {
(tensor_map.find("token_len_per_seq") != tensor_map.end()) &&
(tensor_map.find("inp_pos") != tensor_map.end())) {
auto token_len_per_seq = tensor_map.at("token_len_per_seq").get_node_shared_ptr();
auto mask = tensor_map.at(mask_name).get_node_shared_ptr();
std::shared_ptr<ov::Node> mask_sliced = mask;
@@ -137,6 +139,7 @@ void add_sliced_mask_stateful(TensorMap & tensor_map) {
};
create_sliced_mask("self_kq_mask", "KQ_mask_sliced");
create_sliced_mask("KQ_mask", "KQ_mask_sliced");
create_sliced_mask("self_kq_mask_swa", "KQ_mask_swa_sliced");
}
@@ -395,6 +398,7 @@ std::shared_ptr<Model> TranslateSession::apply_transformations(std::shared_ptr<M
// 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});
manager.register_pass<pass::FuseToConv>();
if (ggml_model_decoder->is_stateful()) {
const auto kv_param_res_names = ggml_model_decoder->get_kv_param_res_names();
@@ -72,6 +72,7 @@ OutputVector rename_outputs_with_suffix(const OutputVector & outputs, const std:
name += "_";
name += suffix;
node->set_friendly_name(name);
// Uncomment to dump every node's inferred shape (used to hunt down dynamic dims on NPU).
// std::cout << name << " " << output.get_partial_shape() << std::endl;
}
return outputs;
+115 -40
View File
@@ -16,6 +16,8 @@
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <functional>
#include <future>
#include <iomanip>
#include <iostream>
#include <memory>
@@ -48,7 +50,7 @@ enum ggml_status ov_graph_compute(ggml_cgraph * cgraph, ggml_backend_t backend)
GgmlOvDecoder::dump_cgraph(cgraph, filename);
}
const auto is_static = ggml_openvino_is_npu();
const auto is_static = ggml_openvino_is_npu() || ggml_openvino_getenv_int("GGML_OPENVINO_FORCE_STATIC");
GGML_ASSERT(ctx->runtime_context != nullptr);
std::shared_ptr<ov_runtime_context> r_ctx = std::static_pointer_cast<ov_runtime_context>(ctx->runtime_context);
@@ -168,13 +170,24 @@ ov::Tensor create_ov_output_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder,
auto output_type = ggml_decoder->get_ov_type(ggml_tensor);
ov::Shape output_shape;
void * output_data = ggml_tensor->data;
if (ggml_decoder->is_static()) {
output_shape = infer_request->get_output_tensor(output_index).get_shape();
} else {
output_shape = ggml_decoder->get_shape(ggml_tensor);
// For a CPY into a padded view_src (e.g. a padded KV cache buffer), the
// OV ScatterUpdate node outputs the full view_src shape, not the CPY node's
// own (smaller) shape. Using the CPY shape here causes set_output_tensor to
// fail with a shape-incompatibility error. Use view_src's shape and data
// pointer instead so the OV tensor matches the model output exactly.
if (ggml_tensor->op == GGML_OP_CPY && ggml_tensor->view_src != nullptr &&
ggml_nbytes(ggml_tensor) != ggml_nbytes(ggml_tensor->view_src)) {
output_shape = ggml_decoder->get_shape(ggml_tensor->view_src);
output_data = ggml_tensor->view_src->data;
} else {
output_shape = ggml_decoder->get_shape(ggml_tensor);
}
}
ov::Tensor output_tensor(output_type, output_shape, ggml_tensor->data);
ov::Tensor output_tensor(output_type, output_shape, output_data);
return output_tensor;
}
@@ -583,7 +596,9 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
return chunk_size;
};
static std::string device = "NPU";
// Normally NPU, but honors GGML_OPENVINO_DEVICE so GGML_OPENVINO_FORCE_STATIC can run the
// static-shape path on CPU/GPU to isolate translation bugs from NPUW/NPU-driver issues.
static std::string device = ggml_openvino_get_device_name();
static auto is_static = true;
static auto stateful = false;
@@ -603,7 +618,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
std::tie(m_params, c_params) = GgmlOvDecoder::compute_llm_params(cgraph, is_static);
const auto * inp_pos = get_inp_pos_tensor(cgraph);
const auto is_prefill = get_is_prefill(inp_pos);
const auto is_prefill = get_is_prefill(cgraph, inp_pos);
graph_key key(cgraph);
static const bool cache_enabled = !ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_CACHE");
bool cache_hit = false;
@@ -687,38 +702,55 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
stateful, false, false, prefill_chunk_size);
decoder_end_time = ggml_time_us();
auto input_model_prefill = std::make_shared<ov::frontend::ggml::InputModel>(ggml_decoder_prefill);
auto input_model_decode = std::make_shared<ov::frontend::ggml::InputModel>(ggml_decoder_decode);
const bool dump_ir = ggml_openvino_getenv_int("GGML_OPENVINO_DUMP_IR");
const auto dump_ir_timestamp = static_cast<long long>(ggml_time_us());
auto model_prefill = ov::frontend::ggml::FrontEnd::convert(input_model_prefill);
ggml_decoder_prefill->clear_model_weights();
auto model_decode = ov::frontend::ggml::FrontEnd::convert(input_model_decode);
ggml_decoder_decode->clear_model_weights();
conversion_end_time = ggml_time_us();
auto build_static_model = [&core, &config, dump_ir, dump_ir_timestamp](
std::shared_ptr<GgmlOvDecoder> decoder,
const char * tag,
std::shared_ptr<ov::Model> & model,
ov::CompiledModel & compiled_model,
std::shared_ptr<ov::InferRequest> & infer_request,
int64_t & local_conversion_end_time,
int64_t & local_compile_end_time) {
auto input_model = std::make_shared<ov::frontend::ggml::InputModel>(decoder);
model = ov::frontend::ggml::FrontEnd::convert(input_model);
decoder->clear_model_weights();
local_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_prefill_%lld.xml", timestamp);
ov::serialize(model_prefill, timestamped_filename);
snprintf(timestamped_filename, sizeof(timestamped_filename), "model_decode_%lld.xml", timestamp);
ov::serialize(model_decode, timestamped_filename);
}
if (dump_ir) {
char timestamped_filename[64];
snprintf(timestamped_filename, sizeof(timestamped_filename), "model_%s_%lld.xml", tag,
dump_ir_timestamp);
ov::serialize(model, timestamped_filename);
}
compiled_model = core.compile_model(model, device, config);
infer_request = std::make_shared<ov::InferRequest>(compiled_model.create_infer_request());
local_compile_end_time = ggml_time_us();
};
std::shared_ptr<ov::Model> model_prefill;
std::shared_ptr<ov::Model> model_decode;
ov::CompiledModel compiled_model_prefill;
ov::CompiledModel compiled_model_decode;
auto remote_context = ggml_openvino_get_remote_context();
if (remote_context.has_value()) {
compiled_model_prefill = core.compile_model(model_prefill, remote_context.value(), config);
compiled_model_decode = core.compile_model(model_decode, remote_context.value(), config);
} else {
compiled_model_prefill = core.compile_model(model_prefill, device, config);
compiled_model_decode = core.compile_model(model_decode, device, config);
}
auto infer_request_prefill = std::make_shared<ov::InferRequest>(compiled_model_prefill.create_infer_request());
auto infer_request_decode = std::make_shared<ov::InferRequest>(compiled_model_decode.create_infer_request());
compile_end_time = ggml_time_us();
std::shared_ptr<ov::InferRequest> infer_request_prefill;
std::shared_ptr<ov::InferRequest> infer_request_decode;
int64_t prefill_conversion_end_time;
int64_t decode_conversion_end_time;
int64_t prefill_compile_end_time;
int64_t decode_compile_end_time;
auto prefill_future = std::async(std::launch::async, build_static_model, ggml_decoder_prefill, "prefill",
std::ref(model_prefill), std::ref(compiled_model_prefill),
std::ref(infer_request_prefill), std::ref(prefill_conversion_end_time),
std::ref(prefill_compile_end_time));
auto decode_future = std::async(std::launch::async, build_static_model, ggml_decoder_decode, "decode",
std::ref(model_decode), std::ref(compiled_model_decode),
std::ref(infer_request_decode), std::ref(decode_conversion_end_time),
std::ref(decode_compile_end_time));
prefill_future.get();
decode_future.get();
conversion_end_time = std::max(prefill_conversion_end_time, decode_conversion_end_time);
compile_end_time = std::max(prefill_compile_end_time, decode_compile_end_time);
model = is_prefill ? model_prefill : model_decode;
ggml_decoder = is_prefill ? ggml_decoder_prefill : ggml_decoder_decode;
@@ -742,7 +774,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
}
if (is_prefill) {
auto inp_len = inp_pos->ne[0];
auto inp_len = get_inp_pos_n_tokens(cgraph, inp_pos);
for (int chunk_index = 0; chunk_index * prefill_chunk_size < inp_len; chunk_index++) {
for (size_t i = 0; i < ov_input_names_local.size(); i++) {
auto param_name = ov_input_names_local[i];
@@ -762,6 +794,11 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
continue;
}
auto * ggml_tensor = model_output_it->second;
if (ggml_nbytes(ggml_tensor) == 0) {
// Zero-row in-place writeback (e.g. the empty s_copy defrag remainder). The OV
// Result is the full cache, so binding it over this 0-byte buffer overflows it.
continue;
}
auto output_tensor = create_ov_output_tensor(ggml_decoder, infer_request, i, ggml_tensor);
infer_request->set_output_tensor(i, output_tensor);
}
@@ -798,6 +835,9 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, std::shared_ptr<o
continue;
}
auto * ggml_tensor = model_output_it->second;
if (ggml_nbytes(ggml_tensor) == 0) {
continue;
}
auto output_tensor = create_ov_output_tensor(ggml_decoder, infer_request, i, ggml_tensor);
infer_request->set_output_tensor(i, output_tensor);
}
@@ -1074,6 +1114,9 @@ ov::Tensor get_ov_input_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder, cons
ov::Tensor get_ov_input_tensor_static_decode(std::shared_ptr<GgmlOvDecoder> ggml_decoder,
const std::string & param_name) {
// NPU decoding stage
if (ggml_decoder->get_model_extra_inputs().count(param_name)) {
return get_ov_input_tensor(ggml_decoder, param_name);
}
const auto * ggml_tensor = ggml_decoder->get_input_ggml_tensor(param_name);
const auto * op = ggml_decoder->get_tensor_used_op(ggml_tensor);
@@ -1123,14 +1166,30 @@ ov::Tensor get_ov_input_tensor_static_prefill(std::shared_ptr<GgmlOvDecoder> ggm
const std::string & param_name,
int chunk_index) {
// NPU prompt processing stage
const auto * ggml_tensor = ggml_decoder->get_input_ggml_tensor(param_name);
const auto * op = ggml_decoder->get_tensor_used_op(ggml_tensor);
const size_t input_len = ggml_decoder->get_input_len();
const size_t chunk_size = ggml_decoder->m_prefill_chunk_size;
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 (param_name == "chunk_valid_len") {
ov::Tensor input_tensor(ov::element::i64, ov::Shape{1});
*input_tensor.data<int64_t>() = (int64_t) chunk_valid_size;
return input_tensor;
}
if (chunk_index > 0 && param_name == "cache_rs_reset_len") {
// The recurrent-state clear belongs to the start of the sequence. Re-applying it on every
// chunk would wipe the state accumulated by the preceding chunks, so disable it (a zero
// length makes scale.cpp's keep-mask select every slot) after the first chunk.
ov::Tensor input_tensor(ov::element::i64, ov::Shape{1});
*input_tensor.data<int64_t>() = 0;
return input_tensor;
}
if (ggml_decoder->get_model_extra_inputs().count(param_name)) {
return get_ov_input_tensor(ggml_decoder, param_name);
}
const auto * ggml_tensor = ggml_decoder->get_input_ggml_tensor(param_name);
const auto * op = ggml_decoder->get_tensor_used_op(ggml_tensor);
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.
@@ -1306,7 +1365,7 @@ void print_input_tensor_info(const std::string & name, const ov::Tensor & tensor
<< std::endl;
switch (tensor.get_element_type()) {
case ov::element::f32: {
if (name.find("self_kq_mask") == std::string::npos) {
if (name.find("self_kq_mask") == std::string::npos && name.find("KQ_mask") == std::string::npos) {
std::cout << *(tensor.data<float>()) << std::endl;
} else {
size_t rows = tensor.get_shape()[2];
@@ -1414,8 +1473,24 @@ const ggml_tensor * get_inp_pos_tensor(ggml_cgraph * cgraph) {
throw std::runtime_error("get_inp_pos_tensor: inp_pos not found in cgraph");
}
bool get_is_prefill(const ggml_tensor * inp_pos) {
return inp_pos->ne[0] > 1;
int64_t get_inp_pos_n_tokens(ggml_cgraph * cgraph, const ggml_tensor * inp_pos) {
// IMROPE stacks n_planes (t/h/w/e) position planes into inp_pos, so ne[0] is
// n_planes * n_tokens. Callers that need a token count must divide the planes out.
int n_planes = 1;
for (int i = 0; i < cgraph->n_nodes; ++i) {
auto * op = cgraph->nodes[i];
for (int j = 0; j < GGML_MAX_SRC; ++j) {
if (op->src[j] == inp_pos) {
n_planes = GgmlOvDecoder::get_inp_pos_n_planes(op);
break;
}
}
}
return inp_pos->ne[0] / n_planes;
}
bool get_is_prefill(ggml_cgraph * cgraph, const ggml_tensor * inp_pos) {
return get_inp_pos_n_tokens(cgraph, inp_pos) > 1;
}
#pragma GCC diagnostic pop
+3 -1
View File
@@ -164,7 +164,9 @@ std::vector<T> pad_input(const ggml_tensor * tensor, size_t padded_rows, size_t
const ggml_tensor * get_inp_pos_tensor(struct ggml_cgraph * cgraph);
bool get_is_prefill(const ggml_tensor * inp_pos);
int64_t get_inp_pos_n_tokens(struct ggml_cgraph * cgraph, const ggml_tensor * inp_pos);
bool get_is_prefill(struct ggml_cgraph * cgraph, const ggml_tensor * inp_pos);
ov::Tensor get_ov_input_tensor(std::shared_ptr<GgmlOvDecoder> ggml_decoder, const std::string & param_name);
ov::Tensor get_ov_input_tensor_static_decode(std::shared_ptr<GgmlOvDecoder> ggml_decoder,
+49 -13
View File
@@ -1,3 +1,4 @@
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
@@ -150,7 +151,8 @@ struct sdpa_partition {
// Build + compile the contiguous-input GQA SDPA graph (MatMul->Divide->Add->SoftMax->MatMul), f32 out.
// Mirrors the hardware-verified scratch/onednn_sdpa_probe.cpp build_gqa (partitions=1, sdp_primitive_kernel_t).
static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int seq, int d) {
static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int seq, int d,
const std::array<int64_t, 5> & k_str, const std::array<int64_t, 5> & v_str) try {
using ltype = logical_tensor::layout_type;
using dt = logical_tensor::data_type;
using ldims = logical_tensor::dims;
@@ -158,11 +160,12 @@ static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int
const int rep = H / Hkv;
const ldims q_sz = {1, Hkv, rep, q, d}, kv_sz = {1, Hkv, 1, seq, d}, s_sz = {1, Hkv, rep, q, seq},
sc = {1, 1, 1, 1, 1}, msk = {1, 1, 1, q, seq}, o_sz = {1, Hkv, rep, q, d};
const ldims k_st(k_str.begin(), k_str.end()), v_st(v_str.begin(), v_str.end());
int64_t id = 0;
sdpa_partition E;
auto query = logical_tensor(id++, t, q_sz, ltype::strided);
auto key = logical_tensor(id++, t, kv_sz, ltype::strided);
auto key = logical_tensor(id++, t, kv_sz, k_st);
auto score = logical_tensor(id++, fi, s_sz, ltype::strided);
auto bmm1 = op(id++, op::kind::MatMul, "bmm1");
bmm1.set_attr<bool>(op::attr::transpose_b, true); // key is [.., seq, d]
@@ -184,7 +187,7 @@ static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int
smax.set_attr<std::string>(op::attr::mode, "inf_as_zero");
smax.add_inputs({masked}); smax.add_outputs({probs});
auto value = logical_tensor(id++, t, kv_sz, ltype::strided);
auto value = logical_tensor(id++, t, kv_sz, v_st);
// f16 output is REQUIRED to hit sdp_primitive_kernel_t (the systolic micro-kernel); an f32 output
// falls to larger_partition_kernel_t which materializes N^2 (confirmed: scratch/onednn_sdpa_kernel_probe.cpp).
// converted to the f32 ggml dst in the permute below.
@@ -198,6 +201,7 @@ static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int
auto parts = g.get_partitions();
if (parts.size() != 1 || !parts[0].is_supported()) {
GGML_LOG_WARN("%s: oneDNN did not fuse the SDPA graph; falling back to TILE kernel\n", __func__);
return E; // ok stays false -> caller falls back to TILE
}
E.ins = parts[0].get_input_ports();
@@ -209,6 +213,12 @@ static sdpa_partition build_sdpa(const engine & eng, int H, int Hkv, int q, int
E.ok = true;
return E;
}
catch (const std::exception & e) {
// compile() can reject a stride set the partitioner never inspects; memoise the failure so the
// fallback costs one build rather than one per call.
GGML_LOG_WARN("%s: oneDNN SDPA partition build failed (%s); falling back to TILE kernel\n", __func__, e.what());
return {};
}
void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tensor * dst) try {
const ggml_tensor * Q = dst->src[0];
@@ -234,13 +244,34 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso
ggml_sycl_pool_alloc<sycl::half> Qf(ctx.pool(), (size_t) H * q * d);
cont_to_f16_sycl<float>((const char *) Q->data, Qf.get(), d, q, H, mb, Q->nb[1], Q->nb[2], Q->nb[3], stream);
// K/V: use pool-alloc for both F16 and dequant paths.
// K/V: bind the f16 cache in place. llama.cpp permutes it to [token][head][dim], so its head
// plane is strided rather than dense, which is what an explicit stride vector expresses.
// Quantized and f32 KV still stage a dense copy -- the layout the k_str/v_str defaults describe.
sycl::half * K_ptr = nullptr;
sycl::half * V_ptr = nullptr;
std::array<int64_t, 5> k_str{ Hkv * seq * d, seq * d, seq * d, d, 1 };
std::array<int64_t, 5> v_str = k_str;
std::optional<ggml_sycl_pool_alloc<sycl::half>> Kf_pool;
std::optional<ggml_sycl_pool_alloc<sycl::half>> Vf_pool;
if (K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16) {
auto bindable = [](const ggml_tensor * t) {
return t->nb[0] == sizeof(sycl::half) && t->nb[1] % sizeof(sycl::half) == 0 &&
t->nb[2] % sizeof(sycl::half) == 0 && t->nb[3] % sizeof(sycl::half) == 0;
};
auto elem_strides = [](const ggml_tensor * t) {
const int64_t s1 = (int64_t) (t->nb[1] / t->nb[0]);
const int64_t s2 = (int64_t) (t->nb[2] / t->nb[0]);
const int64_t s3 = (int64_t) (t->nb[3] / t->nb[0]);
// dims are {mb=1, Hkv, rep=1, seq, d}; the size-1 dims at 0 and 2 never advance an address.
return std::array<int64_t, 5>{ s3, s2, s2, s1, 1 };
};
if (K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16 && bindable(K) && bindable(V)) {
K_ptr = (sycl::half *) K->data;
V_ptr = (sycl::half *) V->data;
k_str = elem_strides(K);
v_str = elem_strides(V);
} else if (K->type == GGML_TYPE_F16 && V->type == GGML_TYPE_F16) {
Kf_pool.emplace(ctx.pool(), (size_t) Hkv * seq * d);
Vf_pool.emplace(ctx.pool(), (size_t) Hkv * seq * d);
cont_to_f16_sycl<sycl::half>((const char *) K->data, Kf_pool->get(), d, seq, Hkv, mb, K->nb[1], K->nb[2], K->nb[3], stream);
@@ -341,19 +372,24 @@ void ggml_sycl_flash_attn_ext_onednn(ggml_backend_sycl_context & ctx, ggml_tenso
ggml_sycl_pool_alloc<sycl::half> outf(ctx.pool(), (size_t) H * q * d); // f16 contiguous SDPA out [mb,H,q,d]
// compile once per (device, shape), reuse across layers/calls.
// compile once per (device, shape, KV strides), reuse across layers/calls. Stride 2 always
// repeats stride 1 and stride 4 is always 1, so the key covers every entry that can differ.
static std::unordered_map<std::string, sdpa_partition> cache;
char keyb[96];
snprintf(keyb, sizeof(keyb), "%d:%lld:%lld:%lld:%lld:%lld", ggml_sycl_get_device(),
(long long) H, (long long) Hkv, (long long) q, (long long) seq, (long long) d);
char keyb[256];
snprintf(keyb, sizeof(keyb), "%d:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld:%lld", ggml_sycl_get_device(),
(long long) H, (long long) Hkv, (long long) q, (long long) seq, (long long) d,
(long long) k_str[0], (long long) k_str[1], (long long) k_str[3],
(long long) v_str[0], (long long) v_str[1], (long long) v_str[3]);
auto it = cache.find(keyb);
if (it == cache.end()) {
it = cache.emplace(keyb, build_sdpa(eng, (int) H, (int) Hkv, (int) q, (int) seq, (int) d)).first;
it = cache.emplace(keyb, build_sdpa(eng, (int) H, (int) Hkv, (int) q, (int) seq, (int) d, k_str, v_str)).first;
}
sdpa_partition & E = it->second;
// _supported() is authoritative: if it accepted this op the partition must build.
// A failure here is a gap in _supported() -- surface it, don't mask it with a fallback.
GGML_ASSERT(E.ok && "oneDNN SDPA partition failed to build for a _supported() shape");
if (!E.ok) {
// oneDNN can decline a shape or a stride set that _supported() never sees; build_sdpa warns per key.
ggml_sycl_flash_attn_ext_tile(ctx, dst);
return;
}
auto id2ptr = [&](size_t r) -> void * {
if (r == E.id_q) return Qf.get();
+5 -1
View File
@@ -104,7 +104,6 @@ enum best_fattn_kernel {
static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const ggml_tensor * dst) {
GGML_UNUSED(device);
#ifndef SYCL_FLASH_ATTN
GGML_UNUSED(dst);
return BEST_FATTN_KERNEL_NONE;
@@ -263,6 +262,11 @@ static best_fattn_kernel ggml_sycl_get_best_fattn_kernel(const int device, const
}
} else {
if (Q->ne[1] <= 2) {
// TILE is faster for quantized KV decode on Xe2 (BMG); keep VEC on untested archs
const gpu_arch arch = ggml_sycl_info().devices[device].hw_info.arch;
if (arch == gpu_arch::intel_gpu_bmg_g21 || arch == gpu_arch::intel_gpu_bmg_g31) {
return BEST_FATTN_KERNEL_TILE;
}
return BEST_FATTN_KERNEL_VEC;
}
}
+152 -4
View File
@@ -767,6 +767,21 @@ static constexpr std::initializer_list<std::array<int, 3>> rms_norm_mul_rope_vie
{ 4, 0, 3 }, // set_rows->src[0] == view
};
static constexpr std::array<ggml_type, 9> lightning_indexer_k_types = {
GGML_TYPE_F32,
GGML_TYPE_F16,
GGML_TYPE_BF16,
GGML_TYPE_Q8_0,
GGML_TYPE_Q5_1,
GGML_TYPE_Q5_0,
GGML_TYPE_Q4_1,
GGML_TYPE_Q4_0,
GGML_TYPE_IQ4_NL,
};
static bool ggml_vk_lightning_indexer_k_type_supported(ggml_type type) {
return std::find(lightning_indexer_k_types.begin(), lightning_indexer_k_types.end(), type) != lightning_indexer_k_types.end();
}
struct vk_device_struct {
std::recursive_mutex mutex;
@@ -1068,6 +1083,7 @@ struct vk_device_struct {
vk_pipeline pipeline_rwkv_wkv6_f32;
vk_pipeline pipeline_rwkv_wkv7_f32;
vk_pipeline pipeline_gated_linear_attn_f32;
vk_pipeline pipeline_lightning_indexer_f32[GGML_TYPE_COUNT];
// [size_idx][kda] where size_idx: 0=d16, 1=d32, 2=d64, 3=d128
vk_pipeline pipeline_gated_delta_net[4][2];
vk_pipeline pipeline_ssm_scan_f32_d128;
@@ -1848,6 +1864,26 @@ struct vk_op_gated_linear_attn_push_constants {
uint32_t H;
float scale;
};
struct vk_op_lightning_indexer_push_constants {
uint32_t n_kv;
uint32_t n_heads;
uint32_t n_tokens;
uint32_t n_streams;
uint32_t n_masks;
uint32_t dispatch_x;
uint32_t q_nb1;
uint32_t q_nb2;
uint32_t q_nb3;
uint32_t k_nb2;
uint32_t k_nb3;
uint32_t w_nb1;
uint32_t w_nb3;
uint32_t m_nb1;
uint32_t m_nb3;
uint32_t d_nb1;
uint32_t d_nb3;
};
static_assert(sizeof(vk_op_lightning_indexer_push_constants) <= 128);
struct vk_op_gated_delta_net_push_constants {
uint32_t H;
uint32_t n_tokens;
@@ -3904,11 +3940,16 @@ static vk_fa_pipeline_state get_fa_pipeline_state(const vk_device& device, const
return vk_fa_pipeline_state{hsk, hsv, params.block_rows, params.block_cols, params.d_split, params.row_split, params.shmem_staging, params.path, params.workgroup_size, subgroup_size, aligned, f32acc, flags, params.limit_occupancy_shmem, k_type, v_type};
}
// Bytes per buffer block for the FaBlockBytesK/V spec constants. F32 is fed as
// a vec4 "block" of 4 floats, everything else uses its ggml block size.
static uint32_t fa_block_bytes(ggml_type t) {
if (t == GGML_TYPE_F32) {
return 16u;
}
return (uint32_t) ggml_type_size(t);
}
static std::vector<uint32_t> get_fa_spec_constants(const vk_fa_pipeline_state& state) {
const auto fa_block_bytes = [](ggml_type t) -> uint32_t {
if (t == GGML_TYPE_F32) return 16u;
return (uint32_t) ggml_type_size(t);
};
return {
/* 0 WorkGroupSize */ state.workgroup_size,
/* 1 Br */ state.Br,
@@ -5847,6 +5888,17 @@ static void ggml_vk_load_shaders(vk_device& device, vk_pipeline requested) {
ggml_vk_create_pipeline(device, device->pipeline_gated_linear_attn_f32, "gated_linear_attn_f32", gated_linear_attn_f32_len, gated_linear_attn_f32_data, "main", 6, sizeof(vk_op_gated_linear_attn_push_constants), {1, 1, 1}, {}, 1);
{
const bool li_subgroup = device->subgroup_arithmetic && device->subgroup_require_full_support;
const size_t li_len = li_subgroup ? lightning_indexer_subgroup_f32_len : lightning_indexer_f32_len;
const void * li_data = li_subgroup ? (const void *)lightning_indexer_subgroup_f32_data : (const void *)lightning_indexer_f32_data;
for (ggml_type k_type : lightning_indexer_k_types) {
const std::string name = "lightning_indexer_" + std::string(ggml_type_name(k_type)) + "_k_f32";
ggml_vk_create_pipeline(device, device->pipeline_lightning_indexer_f32[k_type], name.c_str(), li_len, li_data, "main", 5, sizeof(vk_op_lightning_indexer_push_constants), {1, 1, 1}, {(uint32_t)k_type, fa_block_bytes(k_type), device->subgroup_size}, 1, true, li_subgroup);
}
}
{
const uint32_t gdn_sizes[] = {16, 32, 64, 128};
const char * gdn_names[][2] = {
@@ -11697,6 +11749,12 @@ static vk_pipeline ggml_vk_op_get_pipeline(ggml_backend_vk_context * ctx, const
return ctx->device->pipeline_gated_linear_attn_f32;
}
return nullptr;
case GGML_OP_LIGHTNING_INDEXER:
// only the k type selects a pipeline, the other types are fixed by ggml_lightning_indexer()
if (ggml_vk_lightning_indexer_k_type_supported(src1->type)) {
return ctx->device->pipeline_lightning_indexer_f32[src1->type];
}
return nullptr;
case GGML_OP_GATED_DELTA_NET:
if (src0->type == GGML_TYPE_F32 && dst->type == GGML_TYPE_F32) {
const uint32_t S_v = dst->src[2]->ne[0];
@@ -12772,6 +12830,55 @@ static void ggml_vk_gated_linear_attn(ggml_backend_vk_context * ctx, vk_context&
pc, { (uint32_t)(n_seqs * n_heads), 1, 1 });
}
static void ggml_vk_lightning_indexer(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) {
const ggml_tensor * q = dst->src[0];
const ggml_tensor * k = dst->src[1];
const ggml_tensor * w = dst->src[2];
const ggml_tensor * m = dst->src[3];
vk_pipeline pipeline = ggml_vk_op_get_pipeline(ctx, q, k, w, dst, dst->op);
GGML_ASSERT(pipeline != nullptr);
ggml_pipeline_request_descriptor_sets(ctx, pipeline, 1);
const uint32_t n_kv = k->ne[2];
const uint32_t n_heads = q->ne[1];
const uint32_t n_tokens = q->ne[2];
const uint32_t n_streams = q->ne[3];
const uint32_t n_masks = m->ne[3];
const uint32_t n_outputs = (uint32_t)(dst->ne[0] * dst->ne[1] * dst->ne[3]);
const uint32_t dispatch_x = std::min(n_outputs, ctx->device->properties.limits.maxComputeWorkGroupCount[0]);
const uint32_t dispatch_y = CEIL_DIV(n_outputs, dispatch_x);
// q, w and dst are f32 and m is f16, so their strides are passed in elements;
// k may be quantized, so its strides stay in bytes
const uint32_t q_nb1 = q->nb[1] / sizeof(float);
const uint32_t q_nb2 = q->nb[2] / sizeof(float);
const uint32_t q_nb3 = q->nb[3] / sizeof(float);
const uint32_t k_nb2 = k->nb[2];
const uint32_t k_nb3 = k->nb[3];
const uint32_t w_nb1 = w->nb[1] / sizeof(float);
const uint32_t w_nb3 = w->nb[3] / sizeof(float);
const uint32_t m_nb1 = m->nb[1] / sizeof(ggml_fp16_t);
const uint32_t m_nb3 = m->nb[3] / sizeof(ggml_fp16_t);
const uint32_t d_nb1 = dst->nb[1] / sizeof(float);
const uint32_t d_nb3 = dst->nb[3] / sizeof(float);
const vk_op_lightning_indexer_push_constants pc = {
n_kv, n_heads, n_tokens, n_streams, n_masks, dispatch_x,
q_nb1, q_nb2, q_nb3,
k_nb2, k_nb3,
w_nb1, w_nb3,
m_nb1, m_nb3,
d_nb1, d_nb3,
};
ggml_vk_dispatch_pipeline(ctx, subctx, pipeline,
{ggml_vk_tensor_subbuffer(ctx, q), ggml_vk_tensor_subbuffer(ctx, k), ggml_vk_tensor_subbuffer(ctx, w), ggml_vk_tensor_subbuffer(ctx, m), ggml_vk_tensor_subbuffer(ctx, dst)},
pc, {dispatch_x, dispatch_y, 1});
}
static void ggml_vk_gated_delta_net(ggml_backend_vk_context * ctx, vk_context& subctx, ggml_tensor * dst) {
const ggml_tensor * src_q = dst->src[0];
const ggml_tensor * src_v = dst->src[2];
@@ -15898,6 +16005,11 @@ static bool ggml_vk_build_graph(ggml_backend_vk_context * ctx, ggml_cgraph * cgr
break;
case GGML_OP_LIGHTNING_INDEXER:
ggml_vk_lightning_indexer(ctx, compute_ctx, node);
break;
case GGML_OP_GATED_DELTA_NET:
ggml_vk_gated_delta_net(ctx, compute_ctx, node);
@@ -18676,6 +18788,40 @@ static bool ggml_backend_vk_device_supports_op(ggml_backend_dev_t dev, const ggm
case GGML_OP_GATED_LINEAR_ATTN:
// the shader block size is hardcoded to head_size 64
return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_F32 && op->src[0]->ne[0] == 64;
case GGML_OP_LIGHTNING_INDEXER:
{
const ggml_tensor * q = op->src[0];
const ggml_tensor * k = op->src[1];
const ggml_tensor * w = op->src[2];
const ggml_tensor * m = op->src[3];
// the q/w/m types and the shape relationships between q, k, w, m and dst
// are already asserted in ggml_lightning_indexer()
if (!ggml_vk_lightning_indexer_k_type_supported(k->type) || !device->fp16) {
return false;
}
// the shader block size is hardcoded to head size 128
if (q->ne[0] != 128) {
return false;
}
// the shader indexes the buffers by element stride, and is dispatched
// without allow_misalign
for (const ggml_tensor * t : {q, k, w, m, op}) {
if (t->nb[0] != ggml_type_size(t->type) ||
(vk_tensor_offset(t) + t->view_offs) % device->properties.limits.minStorageBufferOffsetAlignment != 0) {
return false;
}
// the strides get scaled down from bytes, so the division must be exact
for (int i = 1; i < GGML_MAX_DIMS; ++i) {
if (t->nb[i] % ggml_type_size(t->type) != 0) {
return false;
}
}
}
return true;
}
case GGML_OP_GATED_DELTA_NET:
{
const uint32_t S_v = op->src[2]->ne[0];
@@ -19685,6 +19831,8 @@ static void ggml_vk_check_results_0(ggml_backend_vk_context * ctx, ggml_cgraph *
const float * op_params = (const float *)tensor->op_params;
tensor_clone = ggml_gated_linear_attn(ggml_ctx, src_clone[0], src_clone[1],
src_clone[2], src_clone[3], src_clone[4], op_params[0]);
} else if (tensor->op == GGML_OP_LIGHTNING_INDEXER) {
tensor_clone = ggml_lightning_indexer(ggml_ctx, src_clone[0], src_clone[1], src_clone[2], src_clone[3]);
} else if (tensor->op == GGML_OP_GATED_DELTA_NET) {
tensor_clone = ggml_gated_delta_net(ggml_ctx, src_clone[0], src_clone[1],
src_clone[2], src_clone[3], src_clone[4], src_clone[5],
@@ -0,0 +1,55 @@
#if !defined(GGML_FA_TYPES_COMP)
#define GGML_FA_TYPES_COMP
// FaTypeK / FaTypeV spec constant values. These mirror enum ggml_type so the
// host can pass the type directly. Keep in sync with ggml.h.
#define FA_TYPE_F32 0u
#define FA_TYPE_F16 1u
#define FA_TYPE_Q4_0 2u
#define FA_TYPE_Q4_1 3u
#define FA_TYPE_Q5_0 6u
#define FA_TYPE_Q5_1 7u
#define FA_TYPE_Q8_0 8u
#define FA_TYPE_IQ4_NL 20u
#define FA_TYPE_BF16 30u
// Number of matrix elements per buffer block, derived from the K/V type spec
// constant. F32 is treated as a vec4 "block" of 4 floats. F16 uses block size 1
// and bypasses the dequant path entirely. Quants follow their ggml block sizes.
uint fa_block_elems(uint ty) {
switch (ty) {
case FA_TYPE_F32: return 4u;
case FA_TYPE_F16: return 1u;
case FA_TYPE_Q4_0: return uint(QUANT_K_Q4_0);
case FA_TYPE_Q4_1: return uint(QUANT_K_Q4_1);
case FA_TYPE_Q5_0: return uint(QUANT_K_Q5_0);
case FA_TYPE_Q5_1: return uint(QUANT_K_Q5_1);
case FA_TYPE_Q8_0: return uint(QUANT_K_Q8_0);
case FA_TYPE_IQ4_NL: return uint(QUANT_K_IQ4_NL);
case FA_TYPE_BF16: return 1u;
default: return 1u;
}
}
// QUANT_R_MMQ for FA-eligible K types. Q4_*/Q5_* store two nibbles per byte
// (R==2); Q8_0 stores one byte per element (R==1). Used to derive the number
// of int32s per 32-element block on the MMQ K path: ints_per_block == 8 / R.
uint fa_quant_r_mmq(uint ty) {
switch (ty) {
case FA_TYPE_Q4_0: return uint(QUANT_R_Q4_0);
case FA_TYPE_Q4_1: return uint(QUANT_R_Q4_1);
case FA_TYPE_Q5_0: return uint(QUANT_R_Q5_0);
case FA_TYPE_Q5_1: return uint(QUANT_R_Q5_1);
case FA_TYPE_Q8_0: return uint(QUANT_R_Q8_0);
default: return 1u;
}
}
bool fa_type_needs_shmem(uint ty) {
switch (ty) {
case FA_TYPE_IQ4_NL: return true;
default: return false;
}
}
#endif // !defined(GGML_FA_TYPES_COMP)
@@ -88,17 +88,7 @@ layout (binding = 6) readonly buffer MO {uint32_t data_mask_opt[];};
#define BINDING_IDX_K 0
#define BINDING_IDX_V 1
// FaTypeK / FaTypeV spec constant values. These mirror enum ggml_type so the
// host can pass the type directly. Keep in sync with ggml.h.
#define FA_TYPE_F32 0u
#define FA_TYPE_F16 1u
#define FA_TYPE_Q4_0 2u
#define FA_TYPE_Q4_1 3u
#define FA_TYPE_Q5_0 6u
#define FA_TYPE_Q5_1 7u
#define FA_TYPE_Q8_0 8u
#define FA_TYPE_IQ4_NL 20u
#define FA_TYPE_BF16 30u
#include "fa_types.glsl"
#if defined(BFLOAT16)
#define O_TYPE float
@@ -108,45 +98,6 @@ layout (binding = 6) readonly buffer MO {uint32_t data_mask_opt[];};
#define O_TYPEV4 FLOAT_TYPEV4
#endif
// Number of matrix elements per buffer block, derived from the K/V type spec
// constant. F32 is treated as a vec4 "block" of 4 floats. F16 uses block size 1
// and bypasses the dequant path entirely. Quants follow their ggml block sizes.
uint fa_block_elems(uint ty) {
switch (ty) {
case FA_TYPE_F32: return 4u;
case FA_TYPE_F16: return 1u;
case FA_TYPE_Q4_0: return uint(QUANT_K_Q4_0);
case FA_TYPE_Q4_1: return uint(QUANT_K_Q4_1);
case FA_TYPE_Q5_0: return uint(QUANT_K_Q5_0);
case FA_TYPE_Q5_1: return uint(QUANT_K_Q5_1);
case FA_TYPE_Q8_0: return uint(QUANT_K_Q8_0);
case FA_TYPE_IQ4_NL: return uint(QUANT_K_IQ4_NL);
case FA_TYPE_BF16: return 1u;
default: return 1u;
}
}
// QUANT_R_MMQ for FA-eligible K types. Q4_*/Q5_* store two nibbles per byte
// (R==2); Q8_0 stores one byte per element (R==1). Used to derive the number
// of int32s per 32-element block on the MMQ K path: ints_per_block == 8 / R.
uint fa_quant_r_mmq(uint ty) {
switch (ty) {
case FA_TYPE_Q4_0: return uint(QUANT_R_Q4_0);
case FA_TYPE_Q4_1: return uint(QUANT_R_Q4_1);
case FA_TYPE_Q5_0: return uint(QUANT_R_Q5_0);
case FA_TYPE_Q5_1: return uint(QUANT_R_Q5_1);
case FA_TYPE_Q8_0: return uint(QUANT_R_Q8_0);
default: return 1u;
}
}
bool fa_type_needs_shmem(uint ty) {
switch (ty) {
case FA_TYPE_IQ4_NL: return true;
default: return false;
}
}
// These can't be `const` globals because GLSL forbids function calls in global
// const initializers, even when the spec constants would let the driver fold
// them. Macros expand at the use site and fold after specialization.
@@ -0,0 +1,151 @@
#version 450
#extension GL_EXT_control_flow_attributes : require
#extension GL_EXT_shader_16bit_storage : require
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
#extension GL_KHR_shader_subgroup_basic : enable
#if USE_SUBGROUP_ADD
#extension GL_KHR_shader_subgroup_arithmetic : enable
#endif
#define BINDING_IDX_K 0u
#include "types.glsl"
#include "fa_types.glsl"
#define FaTypeV FA_TYPE_F32
layout(constant_id = 0) const uint FaTypeK = FA_TYPE_F32;
layout(constant_id = 1) const uint FaBlockBytesK = 4;
layout(constant_id = 2) const uint SUBGROUP_SIZE = 32;
#include "flash_attn_dequant.glsl"
// one workgroup computes one output element, one invocation per head element
#define HEAD_SIZE 128
layout(local_size_x = HEAD_SIZE, local_size_y = 1, local_size_z = 1) in;
layout(binding = 0) readonly buffer QBuf { float q[]; };
layout(binding = 1) readonly buffer KBufF16 { float16_t k_f16[]; };
layout(binding = 1) readonly buffer KBufF32 { float k_f32[]; };
layout(binding = 1) readonly buffer KBufBF16 { uint16_t k_bf16[]; };
layout(binding = 2) readonly buffer WBuf { float weights[]; };
layout(binding = 3) readonly buffer MBuf { float16_t mask[]; };
layout(binding = 4) writeonly buffer DstBuf { float dst[]; };
layout(push_constant) uniform PushConstants {
uint n_kv;
uint n_heads;
uint n_tokens;
uint n_streams;
uint n_masks;
uint dispatch_x;
uint q_nb1;
uint q_nb2;
uint q_nb3;
uint k_nb2;
uint k_nb3;
uint w_nb1;
uint w_nb3;
uint m_nb1;
uint m_nb3;
uint d_nb1;
uint d_nb3;
};
shared float k_row[HEAD_SIZE];
#if USE_SUBGROUP_ADD
shared float sg_partials[HEAD_SIZE / SUBGROUP_SIZE];
#else
shared float partials[HEAD_SIZE];
#endif
void main() {
const uint tid = gl_LocalInvocationID.x;
const uint output_idx = gl_WorkGroupID.y * dispatch_x + gl_WorkGroupID.x;
const uint n_outputs = n_kv * n_tokens * n_streams;
if (fa_type_needs_shmem(FaTypeK)) {
init_iq_shmem(gl_WorkGroupSize);
}
if (output_idx >= n_outputs) {
return;
}
const uint ik = output_idx % n_kv;
const uint ts = output_idx / n_kv;
const uint t = ts % n_tokens;
const uint s = ts / n_tokens;
const uint k_offset = ik * k_nb2 + s * k_nb3;
// k strides come in as bytes, so scale them down to the view being indexed
const uint k_block_elems = fa_block_elems(FaTypeK);
const uint k_elem_bytes = FaBlockBytesK / k_block_elems;
if (FaTypeK == FA_TYPE_F16) {
k_row[tid] = float(k_f16[k_offset / k_elem_bytes + tid]);
} else if (FaTypeK == FA_TYPE_F32) {
k_row[tid] = k_f32[k_offset / k_elem_bytes + tid];
} else if (FaTypeK == FA_TYPE_BF16) {
k_row[tid] = bf16_to_fp32(uint(k_bf16[k_offset / k_elem_bytes + tid]));
} else if (4 * tid < HEAD_SIZE) {
const uint coord = 4 * tid;
const uint ib = coord / k_block_elems;
const uint iqs = coord % k_block_elems;
const vec4 values = dequantize4(ib, iqs, k_offset / FaBlockBytesK, BINDING_IDX_K);
k_row[coord + 0] = values.x;
k_row[coord + 1] = values.y;
k_row[coord + 2] = values.z;
k_row[coord + 3] = values.w;
}
barrier();
const float k_val = k_row[tid];
float score = 0.0;
for (uint h = 0; h < n_heads; ++h) {
const float prod = q[h * q_nb1 + t * q_nb2 + s * q_nb3 + tid] * k_val;
#if USE_SUBGROUP_ADD
const float sg_sum = subgroupAdd(prod);
if (gl_SubgroupInvocationID == 0) {
sg_partials[gl_SubgroupID] = sg_sum;
}
barrier();
if (tid == 0) {
float sum = 0.0;
[[unroll]] for (uint i = 0; i < HEAD_SIZE / SUBGROUP_SIZE; ++i) {
sum += sg_partials[i];
}
score += max(sum, 0.0) * weights[h + t * w_nb1 + s * w_nb3];
}
// the reads above must complete before the next iteration overwrites sg_partials
barrier();
#else
partials[tid] = prod;
barrier();
[[unroll]] for (uint stride = HEAD_SIZE / 2; stride > 0; stride >>= 1) {
if (tid < stride) {
partials[tid] += partials[tid + stride];
}
barrier();
}
if (tid == 0) {
score += max(partials[0], 0.0) * weights[h + t * w_nb1 + s * w_nb3];
}
// the read of partials[0] above must complete before the next iteration
// overwrites partials[tid]
barrier();
#endif
}
if (tid == 0) {
const uint mask_offset = ik + t * m_nb1 + (s % n_masks) * m_nb3;
dst[ik + t * d_nb1 + s * d_nb3] = score + float(mask[mask_offset]);
}
}
@@ -1069,6 +1069,12 @@ void process_shaders() {
string_to_spv("gated_linear_attn_f32", "gla.comp", merge_maps(base_dict, {{"A_TYPE", "float"}}));
// Compile IQ4_NL support in so its shared LUT is available when K uses it.
// K quant type is selected at runtime via the FaTypeK spec constant.
std::map<std::string, std::string> li_dict = {{"FLOAT_TYPE", "float"}, {"FLOAT_TYPEV4", "vec4"}, {"DATA_A_IQ4_NL", "1"}};
string_to_spv("lightning_indexer_f32", "lightning_indexer.comp", li_dict);
string_to_spv("lightning_indexer_subgroup_f32", "lightning_indexer.comp", merge_maps(li_dict, {{"USE_SUBGROUP_ADD", "1"}}));
string_to_spv("rwkv_wkv7_f32", "wkv7.comp", merge_maps(base_dict, {{"A_TYPE", "float"}}));
string_to_spv("gated_delta_net_f32", "gated_delta_net.comp", merge_maps(base_dict, {{"FLOAT_TYPE", "float"}, {"USE_SUBGROUP_ADD", "1"}, {"USE_SUBGROUP_CLUSTERED", "1"}}));
+127
View File
@@ -162,7 +162,12 @@ class Keys:
TARGET_LAYERS = "{arch}.target_layers"
TARGET_HIDDEN_SIZE = "{arch}.target_hidden_size"
BLOCK_SIZE = "{arch}.block_size"
CONV_KERNEL_SIZE = "{arch}.conv_kernel_size"
CONV_GROUP_SIZE = "{arch}.conv_group_size"
SELECTOR_RANK = "{arch}.selector_rank"
SELECTOR_TOP_K = "{arch}.selector_top_k"
SAMPLE_FROM_ANCHOR = "{arch}.sample_from_anchor"
HAS_CONFIDENCE_HEAD = "{arch}.has_confidence_head"
NORM_BEFORE_RESIDUAL = "{arch}.norm_before_residual"
NORM_BEFORE_FC = "{arch}.norm_before_fc"
@@ -225,6 +230,19 @@ class Keys:
COUNT = "{arch}.hyper_connection.count"
SINKHORN_ITERATIONS = "{arch}.hyper_connection.sinkhorn_iterations"
EPSILON = "{arch}.hyper_connection.epsilon"
# absent means the mix projection is full rank (DeepSeek-V4 behaviour)
LOW_RANK = "{arch}.hyper_connection.low_rank"
class PerLayerEmbedding:
LAYERS = "{arch}.ple.layers"
NGRAM_SIZE = "{arch}.ple.ngram_size"
HEADS_PER_NGRAM = "{arch}.ple.heads_per_ngram"
CONV_KERNEL = "{arch}.ple.conv_kernel"
LAYER_MULTIPLIERS = "{arch}.ple.layer_multipliers"
HEAD_OFFSETS = "{arch}.ple.head_offsets"
HEAD_VOCAB_SIZES = "{arch}.ple.head_vocab_sizes"
EOS_TOKEN_ID = "{arch}.ple.eos_token_id"
IMAGE_TOKEN_ID = "{arch}.ple.image_token_id"
class Rope:
DIMENSION_COUNT = "{arch}.rope.dimension_count"
@@ -494,6 +512,7 @@ class MODEL_ARCH(IntEnum):
QWEN3VLMOE = auto()
QWEN35 = auto()
QWEN35MOE = auto()
QWEN4EXP = auto()
PHI2 = auto()
PHI3 = auto()
PHIMOE = auto()
@@ -636,6 +655,9 @@ class MODEL_TENSOR(IntEnum):
HC_HEAD_FN = auto()
HC_HEAD_BASE = auto()
HC_HEAD_SCALE = auto()
HC_HEAD_NORM = auto() # qwen4exp
HC_HEAD_DOWN = auto() # qwen4exp
HC_HEAD_UP = auto() # qwen4exp
ROPE_FREQS = auto()
ROPE_FACTORS_LONG = auto()
ROPE_FACTORS_SHORT = auto()
@@ -780,6 +802,20 @@ class MODEL_TENSOR(IntEnum):
HC_FFN_FN = auto()
HC_FFN_BASE = auto()
HC_FFN_SCALE = auto()
HC_ATTN_NORM = auto() # qwen4exp
HC_ATTN_DOWN = auto() # qwen4exp
HC_ATTN_UP = auto() # qwen4exp
HC_ATTN_INJECT = auto() # qwen4exp
HC_FFN_NORM = auto() # qwen4exp
HC_FFN_DOWN = auto() # qwen4exp
HC_FFN_UP = auto() # qwen4exp
HC_FFN_INJECT = auto() # qwen4exp
PLE_KEY = auto() # qwen4exp
PLE_VALUE = auto() # qwen4exp
PLE_NORM_KEY = auto() # qwen4exp
PLE_NORM_QUERY = auto() # qwen4exp
PLE_NORM_CONV = auto() # qwen4exp
PLE_CONV1D = auto() # qwen4exp
ATTN_COMPRESSOR_WKV = auto()
ATTN_COMPRESSOR_WGATE = auto()
ATTN_COMPRESSOR_APE = auto()
@@ -1146,6 +1182,13 @@ class MODEL_TENSOR(IntEnum):
DSPARK_MARKOV_W1 = auto() # markov head: prev-token embed
DSPARK_MARKOV_W2 = auto() # markov head: bias projection
DSPARK_CONF_PROJ = auto() # confidence head
DFLASH_ATTN_CONV_BASE = auto()
DFLASH_ATTN_CONV_PROJ = auto()
DFLASH_FFN_CONV_BASE = auto()
DFLASH_FFN_CONV_PROJ = auto()
DFLASH_SELECTOR_PREV = auto()
DFLASH_SELECTOR_NEXT = auto()
DFLASH_SELECTOR_HIDDEN = auto()
# lfm2 audio
A_ENC_NORM_CONV = auto()
A_ENC_LINEAR_POS = auto()
@@ -1217,6 +1260,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
MODEL_ARCH.QWEN3VLMOE: "qwen3vlmoe",
MODEL_ARCH.QWEN35: "qwen35",
MODEL_ARCH.QWEN35MOE: "qwen35moe",
MODEL_ARCH.QWEN4EXP: "qwen4exp",
MODEL_ARCH.PHI2: "phi2",
MODEL_ARCH.PHI3: "phi3",
MODEL_ARCH.PHIMOE: "phimoe",
@@ -1358,6 +1402,9 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.HC_HEAD_FN: "output_hc_fn",
MODEL_TENSOR.HC_HEAD_BASE: "output_hc_base",
MODEL_TENSOR.HC_HEAD_SCALE: "output_hc_scale",
MODEL_TENSOR.HC_HEAD_NORM: "output_hc_norm", # qwen4exp
MODEL_TENSOR.HC_HEAD_DOWN: "output_hc_down", # qwen4exp
MODEL_TENSOR.HC_HEAD_UP: "output_hc_up", # qwen4exp
MODEL_TENSOR.ROPE_FREQS: "rope_freqs",
MODEL_TENSOR.ROPE_FACTORS_LONG: "rope_factors_long",
MODEL_TENSOR.ROPE_FACTORS_SHORT: "rope_factors_short",
@@ -1502,6 +1549,20 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.HC_FFN_FN: "blk.{bid}.hc_ffn_fn",
MODEL_TENSOR.HC_FFN_BASE: "blk.{bid}.hc_ffn_base",
MODEL_TENSOR.HC_FFN_SCALE: "blk.{bid}.hc_ffn_scale",
MODEL_TENSOR.HC_ATTN_NORM: "blk.{bid}.hc_attn_norm", # qwen4exp
MODEL_TENSOR.HC_ATTN_DOWN: "blk.{bid}.hc_attn_down", # qwen4exp
MODEL_TENSOR.HC_ATTN_UP: "blk.{bid}.hc_attn_up", # qwen4exp
MODEL_TENSOR.HC_ATTN_INJECT: "blk.{bid}.hc_attn_inject", # qwen4exp
MODEL_TENSOR.HC_FFN_NORM: "blk.{bid}.hc_ffn_norm", # qwen4exp
MODEL_TENSOR.HC_FFN_DOWN: "blk.{bid}.hc_ffn_down", # qwen4exp
MODEL_TENSOR.HC_FFN_UP: "blk.{bid}.hc_ffn_up", # qwen4exp
MODEL_TENSOR.HC_FFN_INJECT: "blk.{bid}.hc_ffn_inject", # qwen4exp
MODEL_TENSOR.PLE_KEY: "blk.{bid}.ple_key", # qwen4exp
MODEL_TENSOR.PLE_VALUE: "blk.{bid}.ple_value", # qwen4exp
MODEL_TENSOR.PLE_NORM_KEY: "blk.{bid}.ple_norm_key", # qwen4exp
MODEL_TENSOR.PLE_NORM_QUERY: "blk.{bid}.ple_norm_query", # qwen4exp
MODEL_TENSOR.PLE_NORM_CONV: "blk.{bid}.ple_norm_conv", # qwen4exp
MODEL_TENSOR.PLE_CONV1D: "blk.{bid}.ple_conv1d", # qwen4exp
MODEL_TENSOR.ATTN_COMPRESSOR_WKV: "blk.{bid}.attn_compressor_kv",
MODEL_TENSOR.ATTN_COMPRESSOR_WGATE: "blk.{bid}.attn_compressor_gate",
MODEL_TENSOR.ATTN_COMPRESSOR_APE: "blk.{bid}.attn_compressor_ape",
@@ -1895,6 +1956,13 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.DSPARK_MARKOV_W1: "markov_w1",
MODEL_TENSOR.DSPARK_MARKOV_W2: "markov_w2",
MODEL_TENSOR.DSPARK_CONF_PROJ: "conf_proj",
MODEL_TENSOR.DFLASH_ATTN_CONV_BASE: "blk.{bid}.attn_conv_base",
MODEL_TENSOR.DFLASH_ATTN_CONV_PROJ: "blk.{bid}.attn_conv_proj",
MODEL_TENSOR.DFLASH_FFN_CONV_BASE: "blk.{bid}.ffn_conv_base",
MODEL_TENSOR.DFLASH_FFN_CONV_PROJ: "blk.{bid}.ffn_conv_proj",
MODEL_TENSOR.DFLASH_SELECTOR_PREV: "selector_predecessor",
MODEL_TENSOR.DFLASH_SELECTOR_NEXT: "selector_successor",
MODEL_TENSOR.DFLASH_SELECTOR_HIDDEN: "selector_hidden",
MODEL_TENSOR.D2T: "d2t",
}
@@ -2795,6 +2863,58 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.NEXTN_SHARED_HEAD_HEAD,
MODEL_TENSOR.NEXTN_SHARED_HEAD_NORM,
],
MODEL_ARCH.QWEN4EXP: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT,
# no OUTPUT_NORM / ATTN_NORM / ATTN_POST_NORM: hyper-connections replace every layer norm
MODEL_TENSOR.HC_HEAD_NORM,
MODEL_TENSOR.HC_HEAD_DOWN,
MODEL_TENSOR.HC_HEAD_UP,
MODEL_TENSOR.HC_ATTN_NORM,
MODEL_TENSOR.HC_ATTN_DOWN,
MODEL_TENSOR.HC_ATTN_UP,
MODEL_TENSOR.HC_ATTN_INJECT,
MODEL_TENSOR.HC_FFN_NORM,
MODEL_TENSOR.HC_FFN_DOWN,
MODEL_TENSOR.HC_FFN_UP,
MODEL_TENSOR.HC_FFN_INJECT,
# full attention layers: ATTN_Q holds [q|gate] interleaved per head
MODEL_TENSOR.ATTN_Q,
MODEL_TENSOR.ATTN_Q_NORM,
MODEL_TENSOR.ATTN_K,
MODEL_TENSOR.ATTN_K_NORM,
MODEL_TENSOR.ATTN_V,
MODEL_TENSOR.ATTN_OUT,
MODEL_TENSOR.INDEXER_Q_PROJ,
MODEL_TENSOR.INDEXER_K_PROJ,
MODEL_TENSOR.INDEXER_Q_NORM,
MODEL_TENSOR.INDEXER_K_NORM,
MODEL_TENSOR.ATTN_QKV,
MODEL_TENSOR.ATTN_GATE,
MODEL_TENSOR.SSM_A,
MODEL_TENSOR.SSM_CONV1D,
MODEL_TENSOR.SSM_DT,
MODEL_TENSOR.SSM_NORM,
MODEL_TENSOR.SSM_BETA,
MODEL_TENSOR.SSM_ALPHA,
MODEL_TENSOR.SSM_OUT,
MODEL_TENSOR.FFN_GATE_INP,
MODEL_TENSOR.FFN_GATE_INP_SHEXP,
MODEL_TENSOR.FFN_UP_SHEXP,
MODEL_TENSOR.FFN_DOWN_SHEXP,
MODEL_TENSOR.FFN_GATE_SHEXP,
MODEL_TENSOR.FFN_DOWN_EXP,
MODEL_TENSOR.FFN_UP_EXP,
MODEL_TENSOR.FFN_GATE_EXP,
MODEL_TENSOR.FFN_GATE_UP_EXP,
MODEL_TENSOR.PER_LAYER_TOKEN_EMBD,
MODEL_TENSOR.PLE_KEY,
MODEL_TENSOR.PLE_VALUE,
MODEL_TENSOR.PLE_NORM_KEY,
MODEL_TENSOR.PLE_NORM_QUERY,
MODEL_TENSOR.PLE_NORM_CONV,
MODEL_TENSOR.PLE_CONV1D,
],
MODEL_ARCH.PLAMO: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
@@ -4953,6 +5073,13 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.DSPARK_MARKOV_W1,
MODEL_TENSOR.DSPARK_MARKOV_W2,
MODEL_TENSOR.DSPARK_CONF_PROJ,
MODEL_TENSOR.DFLASH_ATTN_CONV_BASE,
MODEL_TENSOR.DFLASH_ATTN_CONV_PROJ,
MODEL_TENSOR.DFLASH_FFN_CONV_BASE,
MODEL_TENSOR.DFLASH_FFN_CONV_PROJ,
MODEL_TENSOR.DFLASH_SELECTOR_PREV,
MODEL_TENSOR.DFLASH_SELECTOR_NEXT,
MODEL_TENSOR.DFLASH_SELECTOR_HIDDEN,
],
MODEL_ARCH.MISTRAL4: [
MODEL_TENSOR.TOKEN_EMBD,
+55 -1
View File
@@ -467,10 +467,15 @@ class GGUFWriter:
shard_bar.reset(total=(total if total > 0 else None))
# relying on the fact that Python dicts preserve insertion order (since 3.7)
for ti in tensors.values():
for name, ti in tensors.items():
assert ti.tensor is not None # can only iterate once over the tensors
assert ti.tensor.nbytes == ti.nbytes
start = fout.tell()
ti.tensor.tofile(fout)
# a short write here would only surface as a corrupt file at load time
if fout.tell() - start != ti.nbytes:
raise ValueError(
f"tensor {name!r} wrote {fout.tell() - start} bytes, expected {ti.nbytes}")
if shard_bar is not None:
shard_bar.update(ti.nbytes)
if bar is not None:
@@ -993,9 +998,24 @@ class GGUFWriter:
def add_block_size(self, value: int) -> None:
self.add_uint32(Keys.LLM.BLOCK_SIZE.format(arch=self.arch), value)
def add_conv_kernel_size(self, value: int) -> None:
self.add_uint32(Keys.LLM.CONV_KERNEL_SIZE.format(arch=self.arch), value)
def add_conv_group_size(self, value: int) -> None:
self.add_uint32(Keys.LLM.CONV_GROUP_SIZE.format(arch=self.arch), value)
def add_selector_rank(self, value: int) -> None:
self.add_uint32(Keys.LLM.SELECTOR_RANK.format(arch=self.arch), value)
def add_selector_top_k(self, value: int) -> None:
self.add_uint32(Keys.LLM.SELECTOR_TOP_K.format(arch=self.arch), value)
def add_sample_from_anchor(self, value: bool) -> None:
self.add_bool(Keys.LLM.SAMPLE_FROM_ANCHOR.format(arch=self.arch), value)
def add_has_confidence_head(self, value: bool) -> None:
self.add_bool(Keys.LLM.HAS_CONFIDENCE_HEAD.format(arch=self.arch), value)
def add_target_layers(self, value: Sequence[int]) -> None:
self.add_array(Keys.LLM.TARGET_LAYERS.format(arch=self.arch), value)
@@ -1029,6 +1049,40 @@ class GGUFWriter:
def add_hyper_connection_epsilon(self, value: float) -> None:
self.add_float32(Keys.HyperConnection.EPSILON.format(arch=self.arch), value)
def add_hyper_connection_low_rank(self, value: int) -> None:
self.add_uint32(Keys.HyperConnection.LOW_RANK.format(arch=self.arch), value)
def add_ple_layers(self, values: Sequence[int]) -> None:
self.add_array(Keys.PerLayerEmbedding.LAYERS.format(arch=self.arch), values)
def add_ple_ngram_size(self, value: int) -> None:
self.add_uint32(Keys.PerLayerEmbedding.NGRAM_SIZE.format(arch=self.arch), value)
def add_ple_heads_per_ngram(self, value: int) -> None:
self.add_uint32(Keys.PerLayerEmbedding.HEADS_PER_NGRAM.format(arch=self.arch), value)
def add_ple_conv_kernel(self, value: int) -> None:
self.add_uint32(Keys.PerLayerEmbedding.CONV_KERNEL.format(arch=self.arch), value)
# multipliers reach ~2.4e13; default INT32 inference would truncate them
def _add_u64_array(self, key: str, values: Sequence[int]) -> None:
self.add_key_value(key, list(values), GGUFValueType.ARRAY, GGUFValueType.UINT64)
def add_ple_layer_multipliers(self, values: Sequence[int]) -> None:
self._add_u64_array(Keys.PerLayerEmbedding.LAYER_MULTIPLIERS.format(arch=self.arch), values)
def add_ple_head_offsets(self, values: Sequence[int]) -> None:
self._add_u64_array(Keys.PerLayerEmbedding.HEAD_OFFSETS.format(arch=self.arch), values)
def add_ple_head_vocab_sizes(self, values: Sequence[int]) -> None:
self._add_u64_array(Keys.PerLayerEmbedding.HEAD_VOCAB_SIZES.format(arch=self.arch), values)
def add_ple_eos_token_id(self, value: int) -> None:
self.add_uint32(Keys.PerLayerEmbedding.EOS_TOKEN_ID.format(arch=self.arch), value)
def add_ple_image_token_id(self, value: int) -> None:
self.add_uint32(Keys.PerLayerEmbedding.IMAGE_TOKEN_ID.format(arch=self.arch), value)
def add_attention_scale(self, value: float) -> None:
self.add_float32(Keys.Attention.SCALE.format(arch=self.arch), value)
+65
View File
@@ -226,3 +226,68 @@ class LazyNumpyTensor(LazyBase):
return eager.tofile(*args, **kwargs)
# TODO: __array_function__
# Tensor written to file one row-chunk at a time
class LazyChunkedTensor:
def __init__(
self, chunks: list[Callable[[], np.ndarray]], shape: tuple[int, ...], dtype: DTypeLike,
qtype: Any = None, byteswap: bool = False,
):
self._chunks = chunks
self._qtype = qtype
self._byteswap = byteswap
self.shape = tuple(shape)
self.dtype = np.dtype(dtype)
@property
def nbytes(self) -> int:
n = self.dtype.itemsize
for d in self.shape:
n *= d
return n
def numpy(self) -> LazyChunkedTensor:
return self
def __array__(self, *args, **kwargs):
# numpy would otherwise make a 1-element object array of self, and write 8 bytes
raise TypeError("LazyChunkedTensor cannot become an ndarray, it is written in chunks")
def quantize(self, qtype: Any) -> LazyChunkedTensor:
from .constants import GGMLQuantizationType
from .quants import QuantError, quant_shape_to_byte_shape
if qtype == GGMLQuantizationType.F32:
shape, dtype = self.shape, np.dtype(np.float32)
elif qtype == GGMLQuantizationType.F16:
shape, dtype = self.shape, np.dtype(np.float16)
else:
try:
shape, dtype = quant_shape_to_byte_shape(self.shape, qtype), np.dtype(np.uint8)
except ValueError as e:
# raised here and not per chunk, so callers can still fall back to F16
raise QuantError(str(e)) from e
return LazyChunkedTensor(self._chunks, shape, dtype, qtype, self._byteswap)
def byteswap(self, inplace: bool = False) -> LazyChunkedTensor:
if inplace:
raise NotImplementedError("a chunked tensor cannot be byteswapped in place")
return LazyChunkedTensor(self._chunks, self.shape, self.dtype, self._qtype, not self._byteswap)
def tofile(self, *args, **kwargs) -> None:
from .quants import quantize
written = 0
for load_chunk in self._chunks:
chunk = load_chunk()
if self._qtype is not None:
# exact only because chunks split on rows, and blocks never cross one
chunk = quantize(chunk, self._qtype)
if self._byteswap:
chunk = chunk.byteswap(inplace=False)
chunk.tofile(*args, **kwargs)
written += chunk.nbytes
del chunk
assert written == self.nbytes, f"chunked tensor wrote {written} bytes, expected {self.nbytes}"
+87
View File
@@ -1355,6 +1355,34 @@ class TensorNameMap:
"model.confidence_head.proj", # dspark
),
MODEL_TENSOR.DFLASH_ATTN_CONV_BASE: (
"model.layers.{bid}.attention_conv.base_kernel",
),
MODEL_TENSOR.DFLASH_ATTN_CONV_PROJ: (
"model.layers.{bid}.attention_conv.kernel_projection",
),
MODEL_TENSOR.DFLASH_FFN_CONV_BASE: (
"model.layers.{bid}.mlp_conv.base_kernel",
),
MODEL_TENSOR.DFLASH_FFN_CONV_PROJ: (
"model.layers.{bid}.mlp_conv.kernel_projection",
),
MODEL_TENSOR.DFLASH_SELECTOR_PREV: (
"model.candidate_selector.predecessor_codebook",
),
MODEL_TENSOR.DFLASH_SELECTOR_NEXT: (
"model.candidate_selector.successor_codebook",
),
MODEL_TENSOR.DFLASH_SELECTOR_HIDDEN: (
"model.candidate_selector.hidden_projection",
),
MODEL_TENSOR.CLS: (
"classifier", # jina
"classifier.dense", # roberta
@@ -2680,6 +2708,65 @@ class TensorNameMap:
"model.layers.{bid}.post_attention_layernorm",
),
},
MODEL_ARCH.QWEN4EXP: {
MODEL_TENSOR.HC_ATTN_NORM: (
"model.layers.{bid}.attn_hyper_connection.hc_norm",
),
MODEL_TENSOR.HC_ATTN_DOWN: (
"model.layers.{bid}.attn_hyper_connection.input_mix_weight_down",
),
MODEL_TENSOR.HC_ATTN_UP: (
"model.layers.{bid}.attn_hyper_connection.input_mix_weight_up",
),
MODEL_TENSOR.HC_ATTN_INJECT: (
"model.layers.{bid}.attn_hyper_connection.block_inject_weight",
),
MODEL_TENSOR.HC_FFN_NORM: (
"model.layers.{bid}.mlp_hyper_connection.hc_norm",
),
MODEL_TENSOR.HC_FFN_DOWN: (
"model.layers.{bid}.mlp_hyper_connection.input_mix_weight_down",
),
MODEL_TENSOR.HC_FFN_UP: (
"model.layers.{bid}.mlp_hyper_connection.input_mix_weight_up",
),
MODEL_TENSOR.HC_FFN_INJECT: (
"model.layers.{bid}.mlp_hyper_connection.block_inject_weight",
),
MODEL_TENSOR.HC_HEAD_NORM: (
"model.hyper_connection_mixer.hc_norm",
),
MODEL_TENSOR.HC_HEAD_DOWN: (
"model.hyper_connection_mixer.input_mix_weight_down",
),
MODEL_TENSOR.HC_HEAD_UP: (
"model.hyper_connection_mixer.input_mix_weight_up",
),
MODEL_TENSOR.INDEXER_Q_NORM: (
"model.layers.{bid}.self_attn.indexer.q_layernorm",
),
MODEL_TENSOR.INDEXER_K_NORM: (
"model.layers.{bid}.self_attn.indexer.k_layernorm",
),
MODEL_TENSOR.PLE_KEY: (
"model.layers.{bid}.ple.key_proj",
),
MODEL_TENSOR.PLE_VALUE: (
"model.layers.{bid}.ple.value_proj",
),
MODEL_TENSOR.PLE_NORM_KEY: (
"model.layers.{bid}.ple.norm_key",
),
MODEL_TENSOR.PLE_NORM_QUERY: (
"model.layers.{bid}.ple.norm_query",
),
MODEL_TENSOR.PLE_NORM_CONV: (
"model.layers.{bid}.ple.norm_conv",
),
MODEL_TENSOR.PLE_CONV1D: (
"model.layers.{bid}.ple.conv1d",
),
},
}
mapping: dict[str, tuple[MODEL_TENSOR, str]]
+9
View File
@@ -214,6 +214,12 @@ extern "C" {
LLAMA_API const char * llama_load_mode_name(enum llama_load_mode load_mode);
LLAMA_API enum llama_load_mode llama_load_mode_from_str(const char * str);
enum llama_tensor_read_lazy {
LLAMA_TENSOR_READ_LAZY_OFF = 0, // always read the whole tensor up front
LLAMA_TENSOR_READ_LAZY_AUTO = 1, // lazy only for marked tensors larger than 4 GiB (requires mmap)
LLAMA_TENSOR_READ_LAZY_ON = 2, // read the rows of tensors marked by the arch on demand (requires mmap)
};
enum llama_context_type {
LLAMA_CONTEXT_TYPE_DEFAULT = 0,
LLAMA_CONTEXT_TYPE_MTP = 1,
@@ -315,6 +321,8 @@ extern "C" {
enum llama_split_mode split_mode; // how to split the model across multiple GPUs
enum llama_load_mode load_mode; // how to load the model
enum llama_tensor_read_lazy tensor_read_lazy; // on-demand reading of tensors marked by the arch
// the GPU that is used for the entire model when split_mode is LLAMA_SPLIT_MODE_NONE
int32_t main_gpu;
@@ -437,6 +445,7 @@ extern "C" {
const struct llama_model_kv_override * kv_overrides; // pointer to kv overrides
const struct llama_model_tensor_override * tt_overrides; // pointer to tensor overrides
const int32_t * prune_layers; // pointer to layer indices to prune
size_t max_buf_size; // max bytes of tensor rows kept in memory at once, 0 = default (8 GiB)
} llama_model_quantize_params;
typedef struct llama_logit_bias {
+5 -1
View File
@@ -48,7 +48,11 @@ echo "org/repo: $org_repo"
meta=$(curl -sSLf -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$org_repo/pulls/$PR")
url_remote=$(echo "$meta" | jq -r '.head.repo.clone_url')
if [[ $url_origin =~ ^git@ ]]; then
url_remote=$(echo "$meta" | jq -r '.head.repo.ssh_url')
else
url_remote=$(echo "$meta" | jq -r '.head.repo.clone_url')
fi
head_ref=$(echo "$meta" | jq -r '.head.ref')
echo "url: $url_remote"
+1
View File
@@ -31,6 +31,7 @@ add_library(llama
llama-memory.cpp
llama-memory-hybrid.cpp
llama-memory-hybrid-iswa.cpp
llama-memory-hybrid-idx.cpp
llama-memory-recurrent.cpp
llama-mmap.cpp
llama-model-loader.cpp
+67
View File
@@ -40,6 +40,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_QWEN3VLMOE, "qwen3vlmoe" },
{ LLM_ARCH_QWEN35, "qwen35" },
{ LLM_ARCH_QWEN35MOE, "qwen35moe" },
{ LLM_ARCH_QWEN4EXP, "qwen4exp" },
{ LLM_ARCH_PHI2, "phi2" },
{ LLM_ARCH_PHI3, "phi3" },
{ LLM_ARCH_PHIMOE, "phimoe" },
@@ -293,6 +294,17 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
{ LLM_KV_HYPER_CONNECTION_COUNT, "%s.hyper_connection.count" },
{ LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, "%s.hyper_connection.sinkhorn_iterations" },
{ LLM_KV_HYPER_CONNECTION_EPSILON, "%s.hyper_connection.epsilon" },
{ LLM_KV_HYPER_CONNECTION_LOW_RANK, "%s.hyper_connection.low_rank" },
{ LLM_KV_PLE_LAYERS, "%s.ple.layers" },
{ LLM_KV_PLE_NGRAM_SIZE, "%s.ple.ngram_size" },
{ LLM_KV_PLE_HEADS_PER_NGRAM, "%s.ple.heads_per_ngram" },
{ LLM_KV_PLE_CONV_KERNEL, "%s.ple.conv_kernel" },
{ LLM_KV_PLE_LAYER_MULTIPLIERS, "%s.ple.layer_multipliers" },
{ LLM_KV_PLE_HEAD_OFFSETS, "%s.ple.head_offsets" },
{ LLM_KV_PLE_HEAD_VOCAB_SIZES, "%s.ple.head_vocab_sizes" },
{ LLM_KV_PLE_EOS_TOKEN_ID, "%s.ple.eos_token_id" },
{ LLM_KV_PLE_IMAGE_TOKEN_ID, "%s.ple.image_token_id" },
{ LLM_KV_HASH_LAYER_COUNT, "%s.hash_layer_count" },
@@ -344,6 +356,12 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
{ LLM_KV_NORM_BEFORE_RESIDUAL, "%s.norm_before_residual" },
{ LLM_KV_NORM_BEFORE_FC, "%s.norm_before_fc" },
{ LLM_KV_DFLASH_BLOCK_SIZE, "%s.block_size" },
{ LLM_KV_DFLASH_CONV_KERNEL_SIZE, "%s.conv_kernel_size" },
{ LLM_KV_DFLASH_CONV_GROUP_SIZE, "%s.conv_group_size" },
{ LLM_KV_DFLASH_SELECTOR_RANK, "%s.selector_rank" },
{ LLM_KV_DFLASH_SELECTOR_TOP_K, "%s.selector_top_k" },
{ LLM_KV_SHORTCONV_L_CACHE, "%s.shortconv.l_cache" },
// sentence-transformers dense modules feature dims
{ LLM_KV_DENSE_2_FEAT_IN, "%s.dense_2_feat_in" },
@@ -500,12 +518,29 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
{ LLM_TENSOR_HC_HEAD_FN, "output_hc_fn" },
{ LLM_TENSOR_HC_HEAD_BASE, "output_hc_base" },
{ LLM_TENSOR_HC_HEAD_SCALE, "output_hc_scale" },
{ LLM_TENSOR_HC_HEAD_NORM, "output_hc_norm" },
{ LLM_TENSOR_HC_HEAD_DOWN, "output_hc_down" },
{ LLM_TENSOR_HC_HEAD_UP, "output_hc_up" },
{ LLM_TENSOR_HC_ATTN_FN, "blk.%d.hc_attn_fn" },
{ LLM_TENSOR_HC_ATTN_BASE, "blk.%d.hc_attn_base" },
{ LLM_TENSOR_HC_ATTN_SCALE, "blk.%d.hc_attn_scale" },
{ LLM_TENSOR_HC_FFN_FN, "blk.%d.hc_ffn_fn" },
{ LLM_TENSOR_HC_FFN_BASE, "blk.%d.hc_ffn_base" },
{ LLM_TENSOR_HC_FFN_SCALE, "blk.%d.hc_ffn_scale" },
{ LLM_TENSOR_HC_ATTN_NORM, "blk.%d.hc_attn_norm" },
{ LLM_TENSOR_HC_ATTN_DOWN, "blk.%d.hc_attn_down" },
{ LLM_TENSOR_HC_ATTN_UP, "blk.%d.hc_attn_up" },
{ LLM_TENSOR_HC_ATTN_INJECT, "blk.%d.hc_attn_inject" },
{ LLM_TENSOR_HC_FFN_NORM, "blk.%d.hc_ffn_norm" },
{ LLM_TENSOR_HC_FFN_DOWN, "blk.%d.hc_ffn_down" },
{ LLM_TENSOR_HC_FFN_UP, "blk.%d.hc_ffn_up" },
{ LLM_TENSOR_HC_FFN_INJECT, "blk.%d.hc_ffn_inject" },
{ LLM_TENSOR_PLE_KEY, "blk.%d.ple_key" },
{ LLM_TENSOR_PLE_VALUE, "blk.%d.ple_value" },
{ LLM_TENSOR_PLE_NORM_KEY, "blk.%d.ple_norm_key" },
{ LLM_TENSOR_PLE_NORM_QUERY, "blk.%d.ple_norm_query" },
{ LLM_TENSOR_PLE_NORM_CONV, "blk.%d.ple_norm_conv" },
{ LLM_TENSOR_PLE_CONV1D, "blk.%d.ple_conv1d" },
{ LLM_TENSOR_ATTN_COMPRESSOR_WKV, "blk.%d.attn_compressor_kv" },
{ LLM_TENSOR_ATTN_COMPRESSOR_WGATE, "blk.%d.attn_compressor_gate" },
{ LLM_TENSOR_ATTN_COMPRESSOR_APE, "blk.%d.attn_compressor_ape" },
@@ -651,6 +686,13 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
{ LLM_TENSOR_DSPARK_MARKOV_W1, "markov_w1" },
{ LLM_TENSOR_DSPARK_MARKOV_W2, "markov_w2" },
{ LLM_TENSOR_DSPARK_CONF_PROJ, "conf_proj" },
{ LLM_TENSOR_DFLASH_ATTN_CONV_BASE, "blk.%d.attn_conv_base" },
{ LLM_TENSOR_DFLASH_ATTN_CONV_PROJ, "blk.%d.attn_conv_proj" },
{ LLM_TENSOR_DFLASH_FFN_CONV_BASE, "blk.%d.ffn_conv_base" },
{ LLM_TENSOR_DFLASH_FFN_CONV_PROJ, "blk.%d.ffn_conv_proj" },
{ LLM_TENSOR_DFLASH_SELECTOR_PREV, "selector_predecessor" },
{ LLM_TENSOR_DFLASH_SELECTOR_NEXT, "selector_successor" },
{ LLM_TENSOR_DFLASH_SELECTOR_HIDDEN, "selector_hidden" },
};
// declare information about the model weight tensors:
@@ -704,12 +746,29 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
{LLM_TENSOR_HC_HEAD_FN, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_HEAD_BASE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_ADD}},
{LLM_TENSOR_HC_HEAD_SCALE, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}},
{LLM_TENSOR_HC_HEAD_NORM, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL}},
{LLM_TENSOR_HC_HEAD_DOWN, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_HEAD_UP, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_ATTN_FN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_ATTN_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}},
{LLM_TENSOR_HC_ATTN_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_HC_FFN_FN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_FFN_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}},
{LLM_TENSOR_HC_FFN_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_HC_ATTN_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_HC_ATTN_DOWN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_ATTN_UP, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_ATTN_INJECT, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_FFN_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_HC_FFN_DOWN, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_FFN_UP, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_HC_FFN_INJECT, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_PLE_KEY, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_PLE_VALUE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_PLE_NORM_KEY, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_PLE_NORM_QUERY, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_PLE_NORM_CONV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_PLE_CONV1D, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_SSM_CONV}},
{LLM_TENSOR_ATTN_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_ATTN_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_ATTN_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
@@ -916,6 +975,13 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
{LLM_TENSOR_DSPARK_MARKOV_W1, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_GET_ROWS}},
{LLM_TENSOR_DSPARK_MARKOV_W2, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_DSPARK_CONF_PROJ, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
{LLM_TENSOR_DFLASH_ATTN_CONV_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_DFLASH_ATTN_CONV_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_DFLASH_FFN_CONV_BASE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_DFLASH_FFN_CONV_PROJ, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
{LLM_TENSOR_DFLASH_SELECTOR_PREV, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_GET_ROWS}},
{LLM_TENSOR_DFLASH_SELECTOR_NEXT, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_GET_ROWS}},
{LLM_TENSOR_DFLASH_SELECTOR_HIDDEN, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
};
LLM_KV::LLM_KV(llm_arch arch, const char * suffix) : arch(arch), suffix(suffix) {}
@@ -1009,6 +1075,7 @@ bool llm_arch_is_hybrid(const llm_arch & arch) {
case LLM_ARCH_KIMI_K3:
case LLM_ARCH_QWEN35:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_QWEN4EXP:
case LLM_ARCH_DEEPSEEK4:
case LLM_ARCH_MINIMAX_01:
return true;
+41
View File
@@ -45,6 +45,7 @@ enum llm_arch {
LLM_ARCH_QWEN3VLMOE,
LLM_ARCH_QWEN35,
LLM_ARCH_QWEN35MOE,
LLM_ARCH_QWEN4EXP,
LLM_ARCH_PHI2,
LLM_ARCH_PHI3,
LLM_ARCH_PHIMOE,
@@ -298,6 +299,17 @@ enum llm_kv {
LLM_KV_HYPER_CONNECTION_COUNT,
LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS,
LLM_KV_HYPER_CONNECTION_EPSILON,
LLM_KV_HYPER_CONNECTION_LOW_RANK,
LLM_KV_PLE_LAYERS,
LLM_KV_PLE_NGRAM_SIZE,
LLM_KV_PLE_HEADS_PER_NGRAM,
LLM_KV_PLE_CONV_KERNEL,
LLM_KV_PLE_LAYER_MULTIPLIERS,
LLM_KV_PLE_HEAD_OFFSETS,
LLM_KV_PLE_HEAD_VOCAB_SIZES,
LLM_KV_PLE_EOS_TOKEN_ID,
LLM_KV_PLE_IMAGE_TOKEN_ID,
LLM_KV_HASH_LAYER_COUNT,
@@ -387,6 +399,11 @@ enum llm_kv {
LLM_KV_TARGET_LAYERS,
LLM_KV_TARGET_HIDDEN_SIZE,
LLM_KV_DFLASH_BLOCK_SIZE,
LLM_KV_DFLASH_CONV_KERNEL_SIZE,
LLM_KV_DFLASH_CONV_GROUP_SIZE,
LLM_KV_DFLASH_SELECTOR_RANK,
LLM_KV_DFLASH_SELECTOR_TOP_K,
LLM_KV_NORM_BEFORE_RESIDUAL,
LLM_KV_NORM_BEFORE_FC,
@@ -565,12 +582,29 @@ enum llm_tensor {
LLM_TENSOR_HC_HEAD_FN,
LLM_TENSOR_HC_HEAD_BASE,
LLM_TENSOR_HC_HEAD_SCALE,
LLM_TENSOR_HC_HEAD_NORM, // qwen4exp
LLM_TENSOR_HC_HEAD_DOWN, // qwen4exp
LLM_TENSOR_HC_HEAD_UP, // qwen4exp
LLM_TENSOR_HC_ATTN_FN,
LLM_TENSOR_HC_ATTN_BASE,
LLM_TENSOR_HC_ATTN_SCALE,
LLM_TENSOR_HC_FFN_FN,
LLM_TENSOR_HC_FFN_BASE,
LLM_TENSOR_HC_FFN_SCALE,
LLM_TENSOR_HC_ATTN_NORM, // qwen4exp
LLM_TENSOR_HC_ATTN_DOWN, // qwen4exp
LLM_TENSOR_HC_ATTN_UP, // qwen4exp
LLM_TENSOR_HC_ATTN_INJECT, // qwen4exp
LLM_TENSOR_HC_FFN_NORM, // qwen4exp
LLM_TENSOR_HC_FFN_DOWN, // qwen4exp
LLM_TENSOR_HC_FFN_UP, // qwen4exp
LLM_TENSOR_HC_FFN_INJECT, // qwen4exp
LLM_TENSOR_PLE_KEY, // qwen4exp
LLM_TENSOR_PLE_VALUE, // qwen4exp
LLM_TENSOR_PLE_NORM_KEY, // qwen4exp
LLM_TENSOR_PLE_NORM_QUERY, // qwen4exp
LLM_TENSOR_PLE_NORM_CONV, // qwen4exp
LLM_TENSOR_PLE_CONV1D, // qwen4exp
LLM_TENSOR_ATTN_COMPRESSOR_WKV,
LLM_TENSOR_ATTN_COMPRESSOR_WGATE,
LLM_TENSOR_ATTN_COMPRESSOR_APE,
@@ -659,6 +693,13 @@ enum llm_tensor {
LLM_TENSOR_DSPARK_MARKOV_W1,
LLM_TENSOR_DSPARK_MARKOV_W2,
LLM_TENSOR_DSPARK_CONF_PROJ,
LLM_TENSOR_DFLASH_ATTN_CONV_BASE,
LLM_TENSOR_DFLASH_ATTN_CONV_PROJ,
LLM_TENSOR_DFLASH_FFN_CONV_BASE,
LLM_TENSOR_DFLASH_FFN_CONV_PROJ,
LLM_TENSOR_DFLASH_SELECTOR_PREV,
LLM_TENSOR_DFLASH_SELECTOR_NEXT,
LLM_TENSOR_DFLASH_SELECTOR_HIDDEN,
};
+91 -8
View File
@@ -661,11 +661,19 @@ void llama_context::sched_reserve() {
// reserve again with pp graph to avoid ggml-alloc reallocations during inference
{
// TODO: not sure if the following graph would be worst case for multi-stream KV caches:
//
// auto * gf = graph_reserve(n_tokens, 1, n_tokens, mctx.get());
//
auto * gf = graph_reserve(n_tokens, n_seqs, n_outputs_pp, mctx.get(), model.hparams.no_alloc);
// TODO: the worst case graph is not always reached for `n_seqs > 1`
// need to implement a more robust mechanism that tries a few different inputs and analyzes the results
ggml_cgraph * gf = nullptr;
switch (model.arch) {
case LLM_ARCH_MINIMAX_01:
// the `inp_diag_decay` tensor size scales with `n_seq_tokens^2` which
// makes `n_seqs == 1` use more memory for the compute graph compared to `n_seqs > 1`
gf = graph_reserve(n_tokens, 1, n_outputs_pp, mctx.get(), model.hparams.no_alloc);
break;
default:
gf = graph_reserve(n_tokens, n_seqs, n_outputs_pp, mctx.get(), model.hparams.no_alloc);
};
if (!gf) {
throw std::runtime_error("failed to allocate compute pp buffers");
}
@@ -2301,12 +2309,17 @@ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
model.arch == LLM_ARCH_BAILINGMOE3 ||
model.arch == LLM_ARCH_QWEN35 ||
model.arch == LLM_ARCH_QWEN35MOE ||
model.arch == LLM_ARCH_QWEN4EXP ||
model.arch == LLM_ARCH_DEEPSEEK4 ||
(model.arch == LLM_ARCH_DFLASH && model.hparams.dsv4_hc_mult > 0) ||
model.arch == LLM_ARCH_NANBEIGE ||
model.arch == LLM_ARCH_MINIMAX_01 ||
model.arch == LLM_ARCH_MINIMAX_M3) {
res = std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors());
} else if (model.arch == LLM_ARCH_DFLASH && model.hparams.dflash_selector_rank > 0) {
// DFlash2's convolutions and selector are shape work rather than matmuls,
// so they cost ~8.6 nodes per tensor against ~5.9 for a plain DFlash draft
res = std::max<uint32_t>(1024u, 12u*model.n_tensors());
} else {
res = std::max<uint32_t>(1024u, 8u*model.n_tensors());
for (const auto & lora : model.loras) {
@@ -2887,13 +2900,83 @@ public:
for (auto & [buft, mbuf] : mbufs_new) {
const auto & mbuf_cur = mbufs.at(buft);
if (!mbuf_cur.buf || mbuf_cur.n_tensors != mbuf.n_tensors || mbuf_cur.total_size != mbuf.total_size) {
if (!mbuf_cur.buf || mbuf_cur.total_size != mbuf.total_size) {
GGML_ABORT("%s: memory buffer mismatch\n", __func__);
}
for (size_t i = 0; i < mbuf_cur.org.size(); ++i) {
ggml_backend_tensor_copy(mbuf_cur.cpy[i], mbuf.org[i]);
if (mbuf_cur.n_tensors == mbuf.n_tensors) {
// same chunking: copy 1:1 by index
for (size_t i = 0; i < mbuf_cur.org.size(); ++i) {
GGML_ASSERT(ggml_nbytes(mbuf_cur.cpy[i]) == ggml_nbytes(mbuf.org[i]));
ggml_backend_tensor_copy(mbuf_cur.cpy[i], mbuf.org[i]);
}
continue;
}
// different chunking: copy the write-side data (mbuf_cur.cpy) into the read-side targets (mbuf.org)
// with a byte cursor. Write and read enumerate the same logical data in the same order but may chunk
// it differently, so copy across tensor boundaries rather than 1:1 by index.
const size_t total = mbuf_cur.total_size;
ggml_init_params params_scratch = {
/*.mem_size =*/ 2*(mbuf_cur.cpy.size() + mbuf.org.size())*ggml_tensor_overhead(),
/*.mem_buffer =*/ NULL,
/*.no_alloc =*/ true,
};
ggml_context * ctx_scratch = ggml_init(params_scratch);
size_t src_pos = 0;
size_t dst_pos = 0;
size_t src_j = 0;
size_t dst_i = 0;
size_t src_base = 0;
size_t dst_base = 0;
while (src_pos < total) {
const auto & src_t = mbuf_cur.cpy[src_j];
const auto & dst_t = mbuf.org[dst_i];
const size_t src_size = ggml_nbytes(src_t);
const size_t dst_size = ggml_nbytes(dst_t);
const size_t src_off = src_pos - src_base;
const size_t dst_off = dst_pos - dst_base;
const size_t n_copy = std::min(src_size - src_off, dst_size - dst_off);
const size_t el = ggml_element_size(src_t);
const int64_t n_el = (int64_t) (n_copy / el);
auto * src_v = ggml_view_1d(ctx_scratch, src_t, n_el, src_off);
ggml_backend_view_init(src_v);
auto * dst_v = ggml_view_1d(ctx_scratch, dst_t, n_el, dst_off);
ggml_backend_view_init(dst_v);
ggml_backend_tensor_copy(src_v, dst_v);
src_pos += n_copy;
dst_pos += n_copy;
if (src_pos - src_base == src_size) {
src_base = src_pos;
++src_j;
}
if (dst_pos - dst_base == dst_size) {
dst_base = dst_pos;
++dst_i;
}
}
GGML_ASSERT(src_pos == total && dst_pos == total);
// any tensors left unvisited hold no data
for (size_t i = src_j; i < mbuf_cur.cpy.size(); ++i) {
GGML_ASSERT(ggml_nbytes(mbuf_cur.cpy[i]) == 0);
}
for (size_t i = dst_i; i < mbuf.org.size(); ++i) {
GGML_ASSERT(ggml_nbytes(mbuf.org[i]) == 0);
}
ggml_free(ctx_scratch);
}
GGML_ASSERT(buf_size == 0);
+2
View File
@@ -120,6 +120,8 @@ LLAMA_API llama_context * llama_get_ctx_other(struct llama_context * ctx);
// model/context data extraction
//
LLAMA_API int32_t llama_model_dflash_selector_top_k(const struct llama_model * model);
// returns pointer to the target-model layer indices
LLAMA_API const int32_t * llama_model_target_layer_ids (const struct llama_model * model);
// returns the number of extracted layers from target model
+22 -1
View File
@@ -201,7 +201,11 @@ uint32_t llama_hparams::n_embd_r() const {
// TODO: maybe support other convolution strides than 1
// NOTE: since the first column of the conv_state is shifted out each time, it's not actually needed
// Corresponds to Mamba's conv_states size
return (ssm_d_conv > 0 ? ssm_d_conv - 1 : 0) * (ssm_d_inner + 2*ssm_n_group*ssm_d_state);
const uint32_t n_conv = (ssm_d_conv > 0 ? ssm_d_conv - 1 : 0) * (ssm_d_inner + 2*ssm_n_group*ssm_d_state);
// PLE conv history needs its own row: Meta splits cache_r_l by head, so a history packed behind the first is unaddressable
// it lives in cache_ple_r_l instead, mirrored like the rest of the PLE module
return n_conv;
}
uint32_t llama_hparams::n_embd_s() const {
@@ -236,6 +240,23 @@ bool llama_hparams::is_recr(uint32_t il) const {
GGML_ABORT("%s: il (%u) out of bounds (n_layer_all: %u)\n", __func__, il, n_layer_all);
}
uint32_t llama_hparams::ple_conv_state() const {
if (ple_n_heads == 0 || ple_conv_kernel == 0) {
return 0;
}
// dilation equals the n-gram size, matching the reference module
return (ple_conv_kernel - 1) * ple_ngram_size * dsv4_hc_mult * n_embd;
}
bool llama_hparams::is_ple(uint32_t il) const {
if (il < n_layer_all) {
return is_ple_impl[il];
}
GGML_ABORT("%s: il (%u) out of bounds (n_layer_all: %u)\n", __func__, il, n_layer_all);
}
uint32_t llama_hparams::n_pos_per_embd() const {
return rope_type == LLAMA_ROPE_TYPE_MROPE || rope_type == LLAMA_ROPE_TYPE_IMROPE ? 4 : 1;
}
+33
View File
@@ -3,12 +3,15 @@
#include "llama.h"
#include <array>
#include <bitset>
#include <cassert>
#include <cmath>
// bump if necessary
#define LLAMA_MAX_LAYERS 512
#define LLAMA_MAX_EXPERTS 1024 // Kimi K3
#define LLAMA_MAX_PLE_NGRAM 8 // qwen4exp
#define LLAMA_MAX_PLE_HEADS 64 // qwen4exp
enum llama_expert_gating_func_type {
LLAMA_EXPERT_GATING_FUNC_TYPE_NONE = 0,
@@ -223,6 +226,12 @@ struct llama_hparams {
// output embedding dimension (0 = use n_embd)
uint32_t n_embd_out_impl = 0;
uint32_t dflash_block_size = 0;
uint32_t dflash_conv_kernel_size = 0;
uint32_t dflash_conv_group_size = 0;
uint32_t dflash_selector_rank = 0;
uint32_t dflash_selector_top_k = 0;
// llama4 smallthinker
uint32_t n_moe_layer_step = 0;
uint32_t n_no_rope_layer_step = 4;
@@ -270,6 +279,30 @@ struct llama_hparams {
float dsv4_hc_eps = 0.0f;
std::array<uint32_t, LLAMA_MAX_LAYERS> dsv4_compress_ratios;
// 0 = full rank (DeepSeek-V4)
uint32_t hc_low_rank = 0;
uint32_t ple_ngram_size = 0;
uint32_t ple_heads_per_ngram = 0;
uint32_t ple_conv_kernel = 0;
uint32_t ple_n_heads = 0; // (ngram_size - 1) * heads_per_ngram
uint32_t ple_head_dim = 0;
uint32_t ple_eos_token_id = 0;
// the id the PLE hash stands in at image positions; 0 makes the loader fall back to EOS
uint32_t ple_image_token_id = 0;
// the file lists PLE layer indices, so this is never a per-layer gguf array and can hold one bit per layer
std::bitset<LLAMA_MAX_LAYERS> is_ple_impl;
// the hash multipliers reach ~2e13 and have to stay 64-bit
std::array<uint64_t, LLAMA_MAX_PLE_NGRAM> ple_layer_multipliers;
// head offsets and vocab sizes are token-space indices; the gather truncates them to int32 anyway
std::array<uint32_t, LLAMA_MAX_PLE_HEADS> ple_head_offsets;
std::array<uint32_t, LLAMA_MAX_PLE_HEADS> ple_head_vocab_sizes;
bool is_ple(uint32_t il) const;
// PLE conv history rows: (kernel - 1) * ngram_size; 0 without a PLE module
uint32_t ple_conv_state() const;
// qwen3vl deepstack
// When parsed from GGUF, this implies the first N layers consume the first
// N deepstack embeddings. Use deepstack_mapping_arr if you need a more
+136 -19
View File
@@ -6,6 +6,7 @@
#include "llama-context.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstring>
@@ -78,7 +79,8 @@ llama_kv_cache::llama_kv_cache(
llama_memory_t mem_other,
const layer_filter_cb & filter,
const layer_reuse_cb & reuse,
const layer_share_cb & share) :
const layer_share_cb & share,
const char * name_tag) :
model(model), hparams(hparams), v_trans(v_trans),
n_seq_max(n_seq_max), n_stream(unified ? 1 : n_seq_max), n_pad(n_pad), n_swa(n_swa), swa_type(swa_type),
other(static_cast<llama_kv_cache *>(mem_other)),
@@ -232,8 +234,8 @@ llama_kv_cache::llama_kv_cache(
ggml_tensor * k = has_k ? ggml_new_tensor_3d(ctx, type_k, n_embd_k_gqa, kv_size, n_stream) : nullptr;
ggml_tensor * v = has_v ? ggml_new_tensor_3d(ctx, type_v, n_embd_v_gqa, kv_size, n_stream) : nullptr;
has_k && ggml_format_name(k, "cache_k_l%d", il);
has_v && ggml_format_name(v, "cache_v_l%d", il);
has_k && ggml_format_name(k, "cache_%sk_l%d", name_tag, il);
has_v && ggml_format_name(v, "cache_%sv_l%d", name_tag, il);
std::vector<ggml_tensor *> k_stream;
std::vector<ggml_tensor *> v_stream;
@@ -1129,7 +1131,7 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch &
cells.pos_set(idx, ubatch.pos[i]);
if (ubatch.is_pos_2d() || ubatch.token) {
if (ubatch.is_pos_2d() || ubatch.token || hparams.ple_n_heads > 0) {
llama_kv_cell_ext ext;
if (ubatch.is_pos_2d()) {
@@ -1139,6 +1141,12 @@ void llama_kv_cache::apply_ubatch(const slot_info & sinfo, const llama_ubatch &
if (ubatch.token) {
ext.tok = ubatch.token[i];
} else if (hparams.ple_n_heads > 0) {
// embd batch (multimodal input) has no token ids, need to pad it with the correct ID for PLE layers
// TODO @ngxson : check if we can do the same as gemma 3n / gemma 4
ext.tok = hparams.ple_image_token_id != 0
? (llama_token) hparams.ple_image_token_id
: (llama_token) hparams.ple_eos_token_id;
}
cells.ext_set(idx, ext);
@@ -1814,7 +1822,8 @@ void llama_kv_cache::set_input_v_rot(ggml_tensor * dst) const {
}
bool llama_kv_cache::has_cell_ext() const {
return hparams.n_pos_per_embd() > 1;
// M-RoPE needs the 2D position, the PLE n-gram hash needs the token id
return hparams.n_pos_per_embd() > 1 || hparams.ple_n_heads > 0;
}
void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const {
@@ -1843,6 +1852,8 @@ void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, st
seqs.set(ubatch.seq_id_unq[s]);
}
const llama_pos w0 = p_min - (llama_pos) n;
// (seq_id, pos) -> token, for every cell that could be a predecessor of a ubatch token
std::unordered_map<uint64_t, llama_token> hist;
@@ -1850,28 +1861,71 @@ void llama_kv_cache::get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, st
return ((uint64_t) seq_id << 32) | (uint32_t) pos;
};
// handle M-RoPE gaps: multiple tokens share the same temporal pos
// TODO @ngxson : improve this in the future
std::array<std::pair<llama_pos, llama_token>, LLAMA_MAX_SEQ> below;
below.fill({ -1, LLAMA_TOKEN_NULL });
for (uint32_t s = 0; s < n_stream; ++s) {
v_cells[s].for_each_token_in(seqs, p_min - (llama_pos) n, p_max,
// p_max inclusive: an embd token looks up cells at its own (shared) position
v_cells[s].for_each_token_in(seqs, 0, p_max + 1,
[&](llama_seq_id seq_id, llama_pos pos, llama_token tok) {
hist[key(seq_id, pos)] = tok;
if (pos >= w0) {
hist[key(seq_id, pos)] = tok;
} else if (pos > below[seq_id].first) {
below[seq_id] = { pos, tok };
}
});
}
// the token at pos p, or the nearest earlier one when p falls in an M-RoPE gap
const auto lookup = [&](llama_seq_id seq_id, llama_pos p) -> llama_token {
for (llama_pos q = p; q >= w0; --q) {
const auto it = hist.find(key(seq_id, q));
if (it != hist.end()) {
return it->second;
}
}
return below[seq_id].second;
};
// an embd (multimodal) ubatch can repeat one position for a whole image, so positions
// do not encode the token order; resolve its predecessors by ubatch order instead
std::vector<uint32_t> ord; // index among the ubatch tokens of the same seq
std::unordered_map<llama_seq_id, std::vector<uint32_t>> seq_idx;
if (!ubatch.token) {
ord.resize(n_tokens);
for (uint32_t i = 0; i < n_tokens; ++i) {
auto & v = seq_idx[ubatch.seq_id[i][0]];
ord[i] = v.size();
v.push_back(i);
}
}
for (uint32_t i = 0; i < n_tokens; ++i) {
// TODO: a token that belongs to more than one sequence has an ambiguous history.
// the n-gram architectures have to reject such batches
const llama_seq_id seq_id = ubatch.seq_id[i][0];
for (uint32_t j = 0; j < n; ++j) {
const llama_pos p = ubatch.pos[i] - (llama_pos) (n - j);
const llama_pos d = (llama_pos) (n - j);
llama_pos p;
if (!ubatch.token) {
const auto & v = seq_idx[seq_id];
const int64_t k = (int64_t) ord[i] - d;
// k >= 0: an earlier token of this very ubatch; k < 0: before the chunk
p = k >= 0 ? ubatch.pos[v[k]] : ubatch.pos[v[0]] + (llama_pos) k;
} else {
p = ubatch.pos[i] - d;
}
if (p < 0) {
continue;
}
const auto it = hist.find(key(seq_id, p));
if (it != hist.end()) {
res[i*n + j] = it->second;
}
res[i*n + j] = lookup(seq_id, p);
}
}
}
@@ -2108,6 +2162,15 @@ void llama_kv_cache::state_write(llama_io_write_i & io, llama_seq_id seq_id, lla
}
void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) {
state_read_sinfo(io, seq_id, flags, nullptr, nullptr);
}
void llama_kv_cache::state_read_sinfo(
llama_io_read_i & io,
llama_seq_id seq_id,
llama_state_seq_flags flags,
slot_info_vec_t * sinfos_out,
const slot_info_vec_t * sinfos_in) {
// TODO: refactor [TAG_KV_CACHE_SHARE_CELLS]
if (other) {
return;
@@ -2118,17 +2181,35 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama
// TODO: fix incosistent handling of `seq_id < 0` and `seq_id == -1` in the codebase [TAG_LLAMA_SEQ_ID_NEG]
GGML_ASSERT(seq_id == -1 || (seq_id >= 0 && (size_t) seq_id < seq_to_stream.size()));
if (sinfos_out) {
sinfos_out->assign(n_stream, slot_info{});
}
if (sinfos_in && sinfos_in->size() != n_stream) {
throw std::runtime_error("failed to restore kv cache: mirrored slot layout has the wrong stream count");
}
uint32_t n_stream_cur;
io.read(&n_stream_cur, sizeof(n_stream_cur));
if (n_stream_cur != n_stream) {
throw std::runtime_error("n_stream mismatch");
}
// a whole-context restore replaces every stream, so the cache is emptied once here
// clear() resets all streams at once, so doing it per stream below would keep only the last one
if (seq_id == -1) {
clear(true);
}
for (uint32_t s = 0; s < n_stream; ++s) {
uint32_t cell_count;
io.read(&cell_count, sizeof(cell_count));
if (cell_count == 0) {
// a mirrored cache must be empty here as well, or the two no longer agree cell for cell
if (sinfos_in && !(*sinfos_in)[s].empty()) {
throw std::runtime_error("failed to restore kv cache: mirrored cache holds cells this one does not");
}
continue;
}
@@ -2137,7 +2218,7 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama
slot_info sinfo;
bool res = true;
res = res && state_read_meta(io, strm, cell_count, sinfo, seq_id);
res = res && state_read_meta(io, strm, cell_count, sinfo, seq_id, sinfos_in ? &(*sinfos_in)[s] : nullptr);
try {
res = res && state_read_data(io, strm, cell_count, sinfo);
@@ -2153,6 +2234,10 @@ void llama_kv_cache::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama
}
throw std::runtime_error("failed to restore kv cache");
}
if (sinfos_out) {
(*sinfos_out)[s] = sinfo;
}
}
}
@@ -2288,7 +2373,7 @@ void llama_kv_cache::state_write_data(llama_io_write_i & io, const cell_ranges_t
}
}
bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32_t cell_count, slot_info & sinfo, llama_seq_id dest_seq_id) {
bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32_t cell_count, slot_info & sinfo, llama_seq_id dest_seq_id, const slot_info * sinfo_in) {
auto & cells = v_cells[strm];
auto & head = v_heads[strm];
@@ -2338,10 +2423,37 @@ bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32
ubatch.seq_id[i] = &dest_seq_id;
}
sinfo = find_slot(ubatch, false);
if (sinfo.empty()) {
LLAMA_LOG_ERROR("%s: failed to find %d available cells in kv cache\n", __func__, cell_count);
return false;
if (sinfo_in) {
// this cache mirrors another one, so it takes that cache's layout instead of searching for its own cells
if (sinfo_in->empty() || sinfo_in->n_stream() != 1 || sinfo_in->idxs[0].size() != cell_count) {
LLAMA_LOG_ERROR("%s: mirrored slot layout holds %d cells, this cache restores %d\n", __func__,
sinfo_in->empty() ? 0 : (int) sinfo_in->idxs[0].size(), cell_count);
return false;
}
sinfo = *sinfo_in;
// the layout is cell indices, so it means the same in both caches only while their streams line up
sinfo.s0 = strm;
sinfo.s1 = strm;
sinfo.strm[0] = strm;
// seq_rm above freed exactly the cells this sequence held
// anything else in the way is a cache that had already drifted, which this restore must not hide
for (uint32_t i = 0; i < cell_count; ++i) {
const uint32_t idx = sinfo.idxs[0][i];
if (idx >= cells.size() || !cells.is_empty(idx)) {
LLAMA_LOG_ERROR("%s: cell %u of the mirrored slot layout is not free\n", __func__, idx);
return false;
}
}
} else {
sinfo = find_slot(ubatch, false);
if (sinfo.empty()) {
LLAMA_LOG_ERROR("%s: failed to find %d available cells in kv cache\n", __func__, cell_count);
return false;
}
}
// note: apply_ubatch() rebuilds llama_kv_cell_ext from the ubatch
@@ -2367,7 +2479,12 @@ bool llama_kv_cache::state_read_meta(llama_io_read_i & io, uint32_t strm, uint32
return false;
}
clear(true);
// the cells go in from 0, so a mirrored cache lands on the same ones as long as it restores the same count. the layout itself carries no more information here
if (sinfo_in && (sinfo_in->empty() || sinfo_in->n_stream() != 1 || sinfo_in->idxs[0].size() != cell_count)) {
LLAMA_LOG_ERROR("%s: mirrored slot layout holds %d cells, this cache restores %d\n", __func__,
sinfo_in->empty() ? 0 : (int) sinfo_in->idxs[0].size(), cell_count);
return false;
}
for (uint32_t i = 0; i < cell_count; ++i) {
llama_pos pos;
+20 -3
View File
@@ -112,7 +112,9 @@ public:
llama_memory_t mem_other,
const layer_filter_cb & filter,
const layer_reuse_cb & reuse,
const layer_share_cb & share);
const layer_share_cb & share,
// a model can hold more than one cache, so the tensor names have to stay unique
const char * name_tag = "");
~llama_kv_cache() = default;
@@ -166,6 +168,17 @@ public:
const llama_kv_cells & get_cells(llama_seq_id seq_id) const;
// state_read, plus the cells the restored tokens were placed in
// a cache that mirrors another one (the qwen4exp indexer) must not search for its own cells: two searches agree only by luck
// sinfos_out: if set, filled with the layout used; a stream with no cells leaves an empty entry
// sinfos_in : if set, the layout to use instead of searching. one entry per stream, cell count must match the blob
void state_read_sinfo(
llama_io_read_i & io,
llama_seq_id seq_id,
llama_state_seq_flags flags,
slot_info_vec_t * sinfos_out,
const slot_info_vec_t * sinfos_in);
//
// graph_build API
//
@@ -223,7 +236,10 @@ public:
bool has_cell_ext() const;
// for every token of the ubatch, the ids of the n tokens that precede it in its sequence
// entries with no matching cell are set to LLAMA_TOKEN_NULL
// example for M-RoPE image case: tokens A B X X X C, where X is a 3-token image at pos 2 spanning positions 2..4:
// tok: A B X X X C
// pos: 0 1 2 2 2 5
// prev, n=2: A -> [NULL, NULL], B -> [NULL, A], 3rd X -> [X, X], C -> [X, X]
// note: used by n-gram input embeddings
void get_prev_tokens(const llama_ubatch & ubatch, uint32_t n, std::vector<llama_token> & res) const;
@@ -326,7 +342,8 @@ private:
void state_write_meta(llama_io_write_i & io, const cell_ranges_t & cr, llama_seq_id seq_id = -1) const;
void state_write_data(llama_io_write_i & io, const cell_ranges_t & cr) const;
bool state_read_meta(llama_io_read_i & io, uint32_t strm, uint32_t cell_count, slot_info & sinfo, llama_seq_id dest_seq_id = -1);
// sinfo_in, when set, replaces the find_slot call: the cells are given by the caller
bool state_read_meta(llama_io_read_i & io, uint32_t strm, uint32_t cell_count, slot_info & sinfo, llama_seq_id dest_seq_id = -1, const slot_info * sinfo_in = nullptr);
bool state_read_data(llama_io_read_i & io, uint32_t strm, uint32_t cell_count, const slot_info & sinfo);
};
+465
View File
@@ -0,0 +1,465 @@
#include "llama-memory-hybrid-idx.h"
#include "llama-impl.h"
#include "llama-batch.h"
#include "llama-io.h"
#include "llama-model.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iterator>
#include <stdexcept>
//
// llama_memory_hybrid_idx
//
llama_memory_hybrid_idx::llama_memory_hybrid_idx(
const llama_model & model,
/* attn */
ggml_type type_k,
ggml_type type_v,
bool v_trans,
uint32_t kv_size,
uint32_t n_pad,
uint32_t n_swa,
llama_swa_type swa_type,
/* recurrent */
ggml_type type_r,
ggml_type type_s,
uint32_t rs_size,
/* common */
uint32_t n_seq_max,
uint32_t n_rs_seq,
bool offload,
bool unified,
/* layer filters */
const layer_filter_cb & filter_attn,
const layer_filter_cb & filter_recr,
const layer_filter_cb & filter_idx) :
llama_memory_hybrid(
model,
type_k, type_v, v_trans, kv_size, n_pad, n_swa, swa_type,
type_r, type_s, rs_size,
n_seq_max, n_rs_seq, offload, unified,
filter_attn, filter_recr),
hparams_idx(model.hparams),
mem_idx(filter_idx == nullptr ? nullptr : [&] {
// MQA with a single key head of indexer_head_size, as llama_kv_cache_dsa shapes its own
std::fill(hparams_idx.n_head_kv_arr.begin(), hparams_idx.n_head_kv_arr.end(), 1);
hparams_idx.n_embd_head_k_full = model.hparams.indexer_head_size;
LLAMA_LOG_INFO("%s: creating indexer KV cache, size = %u cells\n", __func__, kv_size);
return new llama_kv_cache(
model, hparams_idx, type_k, type_v, v_trans, offload, unified,
kv_size, n_seq_max, n_pad, n_swa, swa_type,
nullptr, filter_idx, nullptr, nullptr, "idx_");
}()) {}
llama_memory_context_ptr llama_memory_hybrid_idx::init_batch(llama_batch_allocr & balloc, uint32_t n_ubatch, bool embd_all) {
// note: repeats llama_memory_hybrid::init_batch, as the indexer needs the attention slot infos that the base context hides
do {
balloc.split_reset();
// follow the recurrent pattern for creating the ubatch splits
std::vector<llama_ubatch> ubatches;
while (true) {
llama_ubatch ubatch;
if (embd_all) {
// if all tokens are output, split by sequence
ubatch = balloc.split_seq(n_ubatch);
} else {
// Use non-sequential split when KV cache is unified (needed for hellaswag/winogrande/multiple-choice)
const bool unified = (get_mem_attn()->get_n_stream() == 1);
// [TAG_RECURRENT_ROLLBACK_SPLITS]
// the trailing (1 + n_rs_seq) tokens of each seq must stay in the same ubatch
// so that the rollback snapshots remain valid
const uint32_t n_rs_seq = get_mem_recr()->n_rs_seq;
ubatch = balloc.split_equal(n_ubatch, !unified, n_rs_seq > 0 ? n_rs_seq + 1 : 0);
}
if (ubatch.n_tokens == 0) {
break;
}
ubatches.push_back(std::move(ubatch)); // NOLINT
}
if (balloc.get_n_used() < balloc.get_n_tokens()) {
// failed to find a suitable split
break;
}
// prepare the recurrent batches first
if (!get_mem_recr()->prepare(ubatches)) {
// TODO: will the recurrent cache be in an undefined context at this point?
LLAMA_LOG_ERROR("%s: failed to prepare recurrent ubatches\n", __func__);
return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
}
// prepare the attention cache
auto heads_attn = get_mem_attn()->prepare(ubatches);
if (heads_attn.empty()) {
LLAMA_LOG_ERROR("%s: failed to prepare attention ubatches\n", __func__);
return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
}
// the indexer uses the attention cache's slot layout; a separate one can drift from it
llama_kv_cache::slot_info_vec_t heads_idx;
if (mem_idx) {
heads_idx = heads_attn;
}
return std::make_unique<llama_memory_hybrid_idx_context>(
this, std::move(heads_attn), std::move(heads_idx), std::move(ubatches));
} while(false);
return std::make_unique<llama_memory_hybrid_idx_context>(LLAMA_MEMORY_STATUS_FAILED_PREPARE);
}
llama_memory_context_ptr llama_memory_hybrid_idx::init_full() {
return std::make_unique<llama_memory_hybrid_idx_context>(this);
}
llama_memory_context_ptr llama_memory_hybrid_idx::init_update(llama_context * lctx, bool optimize) {
return std::make_unique<llama_memory_hybrid_idx_context>(this, lctx, optimize);
}
void llama_memory_hybrid_idx::clear(bool data) {
llama_memory_hybrid::clear(data);
if (mem_idx) {
mem_idx->clear(data);
}
}
bool llama_memory_hybrid_idx::seq_rm(llama_seq_id seq_id, llama_pos p0, llama_pos p1) {
// same order as llama_memory_hybrid::seq_rm: the recurrent cache can refuse, so try it first
if (!get_mem_recr()->seq_rm(seq_id, p0, p1)) {
return false;
}
if (mem_idx) {
mem_idx->seq_rm(seq_id, p0, p1);
}
return get_mem_attn()->seq_rm(seq_id, p0, p1);
}
void llama_memory_hybrid_idx::seq_cp(llama_seq_id seq_id_src, llama_seq_id seq_id_dst, llama_pos p0, llama_pos p1) {
llama_memory_hybrid::seq_cp(seq_id_src, seq_id_dst, p0, p1);
if (mem_idx) {
mem_idx->seq_cp(seq_id_src, seq_id_dst, p0, p1);
}
}
void llama_memory_hybrid_idx::seq_keep(llama_seq_id seq_id) {
llama_memory_hybrid::seq_keep(seq_id);
if (mem_idx) {
mem_idx->seq_keep(seq_id);
}
}
void llama_memory_hybrid_idx::seq_add(llama_seq_id seq_id, llama_pos p0, llama_pos p1, llama_pos shift) {
llama_memory_hybrid::seq_add(seq_id, p0, p1, shift);
if (mem_idx) {
mem_idx->seq_add(seq_id, p0, p1, shift);
}
}
void llama_memory_hybrid_idx::seq_div(llama_seq_id seq_id, llama_pos p0, llama_pos p1, int d) {
llama_memory_hybrid::seq_div(seq_id, p0, p1, d);
if (mem_idx) {
mem_idx->seq_div(seq_id, p0, p1, d);
}
}
std::map<ggml_backend_buffer_type_t, size_t> llama_memory_hybrid_idx::memory_breakdown() const {
std::map<ggml_backend_buffer_type_t, size_t> mb = llama_memory_hybrid::memory_breakdown();
if (mem_idx) {
for (const auto & buft_size : mem_idx->memory_breakdown()) {
mb[buft_size.first] += buft_size.second;
}
}
return mb;
}
void llama_memory_hybrid_idx::state_write(llama_io_write_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) const {
llama_memory_hybrid::state_write(io, seq_id, flags);
// [TAG_HYBRID_IDX_STATE] the indexer section goes last, so it is a pure suffix: an old reader stops early instead of misparsing it
// The indexer mirrors the attention cache, so it uses the same PARTIAL_ONLY gate.
if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
if (mem_idx) {
mem_idx->state_write(io, seq_id, flags);
}
}
}
void llama_memory_hybrid_idx::state_read(llama_io_read_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) {
// note: repeats llama_memory_hybrid::state_read
// the indexer needs the attention cache's cells, and a half-failed restore must leave all three caches alike
// [TAG_HYBRID_IDX_SINFO]
// the indexer restore adopts the attention cache's layout instead of searching for cells of its own
// two find_slot calls agree only while both caches see the same occupancy, which a restore cannot promise
llama_kv_cache::slot_info_vec_t sinfos_attn;
try {
if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
get_mem_attn()->state_read_sinfo(io, seq_id, flags, mem_idx ? &sinfos_attn : nullptr, nullptr);
}
get_mem_recr()->state_read(io, seq_id, flags);
// [TAG_HYBRID_IDX_STATE] must mirror the write order in state_write
if ((flags & LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY) == 0) {
if (mem_idx) {
mem_idx->state_read_sinfo(io, seq_id, flags, nullptr, &sinfos_attn);
}
}
} catch (...) {
// a half-restored context is the one state the indexer cannot fix by itself: attention holds new cells, the indexer old ones
// drop what was being restored from all of them, which is a state they do agree on.
state_drop(seq_id);
throw;
}
}
void llama_memory_hybrid_idx::state_drop(llama_seq_id seq_id) {
// dropped directly, not via seq_rm: the recurrent cache may refuse it and then only the other two get cleared
if (seq_id < 0) {
clear(true);
return;
}
get_mem_attn()->seq_rm(seq_id, -1, -1);
get_mem_recr()->seq_rm(seq_id, -1, -1);
if (mem_idx) {
mem_idx->seq_rm(seq_id, -1, -1);
}
}
llama_kv_cache * llama_memory_hybrid_idx::get_mem_idx() const {
return mem_idx.get();
}
//
// llama_memory_hybrid_idx_context
//
// streams in each ubatch's slot info, matching get_k/get_v's `ns`
static std::vector<uint32_t> llama_memory_hybrid_idx_ns(const llama_kv_cache::slot_info_vec_t & sinfos) {
std::vector<uint32_t> res;
res.reserve(sinfos.size());
for (const auto & sinfo : sinfos) {
res.push_back(sinfo.s1 - sinfo.s0 + 1);
}
return res;
}
llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(llama_memory_status status) :
llama_memory_hybrid_context(status) {}
llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(llama_memory_hybrid_idx * mem) :
llama_memory_hybrid_context(mem),
mem(mem),
// graph reservation walks a full context, and qwen4exp builds the sparse attention only when this is set
// without it the reserved worst case is the dense graph, so ggml-alloc must grow the buffer on the first decode
ns_ubatch(mem->get_mem_idx() == nullptr ?
std::vector<uint32_t>() : std::vector<uint32_t>{ mem->get_mem_idx()->get_n_stream() }),
ctx_idx(mem->get_mem_idx() == nullptr ? nullptr :
new llama_kv_cache_context(mem->get_mem_idx())) {}
llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(
llama_memory_hybrid_idx * mem,
llama_context * lctx,
bool optimize) :
llama_memory_hybrid_context(mem, lctx, optimize),
mem(mem) {}
llama_memory_hybrid_idx_context::llama_memory_hybrid_idx_context(
llama_memory_hybrid_idx * mem,
slot_info_vec_t sinfos_attn,
slot_info_vec_t sinfos_idx,
std::vector<llama_ubatch> ubatches) :
// note: the base copies the ubatches; ctx_idx gets a copy of its own
llama_memory_hybrid_context(mem, std::move(sinfos_attn), ubatches),
mem(mem),
ns_ubatch(llama_memory_hybrid_idx_ns(sinfos_idx)),
ctx_idx(mem->get_mem_idx() == nullptr ? nullptr :
new llama_kv_cache_context(mem->get_mem_idx(), std::move(sinfos_idx), ubatches)) {}
bool llama_memory_hybrid_idx_context::next() {
if (ctx_idx) {
ctx_idx->next();
}
++i_cur;
return llama_memory_hybrid_context::next();
}
bool llama_memory_hybrid_idx_context::apply() {
bool res = llama_memory_hybrid_context::apply();
if (ctx_idx) {
res = res & ctx_idx->apply();
}
return res;
}
const llama_kv_cache_context * llama_memory_hybrid_idx_context::get_idx() const {
return static_cast<const llama_kv_cache_context *>(ctx_idx.get());
}
uint32_t llama_memory_hybrid_idx_context::get_n_stream() const {
GGML_ASSERT(i_cur < ns_ubatch.size());
return ns_ubatch[i_cur];
}
void llama_memory_hybrid_idx_context::set_input_qsa(
ggml_tensor * cell_blk,
ggml_tensor * blk_cells,
ggml_tensor * blk_pos,
ggml_tensor * bias,
const llama_ubatch * ubatch,
uint32_t ratio,
bool blk_bias) const {
GGML_ASSERT(ratio > 0);
GGML_ASSERT(mem != nullptr && mem->get_mem_idx() != nullptr);
GGML_ASSERT(ggml_backend_buffer_is_host(cell_blk->buffer));
const int64_t n_kv = cell_blk->ne[0];
const int64_t n_ns = cell_blk->ne[1]; // streams in this ubatch
const int64_t n_blocks = blk_pos->ne[0]/(4*n_ns);
const int64_t n_tokens = ubatch->n_tokens;
const int64_t r = ratio;
GGML_ASSERT(n_tokens % n_ns == 0);
const int64_t n_tps = n_tokens/n_ns; // tokens per stream
int32_t * dst_cell_blk = (int32_t *) cell_blk->data;
int32_t * dst_blk_cells = (int32_t *) blk_cells->data;
int32_t * dst_blk_pos = (int32_t *) blk_pos->data;
float * dst_bias = (float *) bias->data;
// block b covers [b*ratio, (b+1)*ratio), so its first token is at b*ratio
// all mrope sections carry it: exact for text, approximate for images
for (int64_t sec = 0; sec < 4; ++sec) {
for (int64_t s = 0; s < n_ns; ++s) {
for (int64_t b = 0; b < n_blocks; ++b) {
dst_blk_pos[sec*(n_blocks*n_ns) + s*n_blocks + b] = (int32_t) (b*r);
}
}
}
// one pass per stream: cell j is a different token in each, so no mapping is shared
std::vector<int32_t> blk_of(n_kv);
std::vector<int32_t> filled(n_blocks);
for (int64_t s = 0; s < n_ns; ++s) {
// ubatch index s*n_tps belongs to this stream; ask which cells array it uses
const llama_seq_id seq_of_stream = ubatch->seq_id[s*n_tps][0];
const auto & cells = mem->get_mem_idx()->get_cells(seq_of_stream);
int32_t * cur_cell_blk = dst_cell_blk + s*n_kv;
int32_t * cur_blk_cells = dst_blk_cells + s*(r*n_blocks);
// an incomplete block cannot be pooled; the bias below forces those tail cells in
// -1 means no usable block, and block 0 only keeps the gather in range
std::fill(blk_of.begin(), blk_of.end(), -1);
std::fill(filled.begin(), filled.end(), 0);
std::fill(cur_blk_cells, cur_blk_cells + r*n_blocks, 0);
// a cell no block covers needs its own -inf, which a per-block bias cannot carry
// every cache path keeps the position below the cell window, so this stays false
bool oor = false;
for (int64_t j = 0; j < n_kv; ++j) {
if (cells.is_empty(j)) {
continue;
}
const llama_pos p = cells.pos_get(j);
const int64_t b = p/r;
if (b >= n_blocks) {
oor = true;
continue;
}
blk_of[j] = (int32_t) b;
cur_blk_cells[b*r + (p%r)] = (int32_t) j;
filled[b]++;
}
GGML_ASSERT((!blk_bias || !oor) && "qsa: cell position runs past the cell window");
// per-block mode keeps an unpooled cell's real block, so the block's own -inf reaches it
// per-cell mode carries that -inf itself and only needs the gather in range
for (int64_t j = 0; j < n_kv; ++j) {
if (blk_of[j] >= 0 && filled[blk_of[j]] < r && !blk_bias) {
blk_of[j] = -1;
}
cur_cell_blk[j] = blk_of[j] < 0 ? 0 : blk_of[j];
}
for (int64_t ii = 0; ii < n_tps; ++ii) {
const int64_t i = s*n_tps + ii;
const llama_seq_id seq_id = ubatch->seq_id[i][0];
const llama_pos q = ubatch->pos[i];
// the tail is an incomplete block and is always visible, as in the reference
const llama_pos tail_start = (q + 1)/r*r;
if (blk_bias) {
// a block sits wholly inside or outside the tail, so one value covers it
// the caller adds the attention mask, which drops empty, foreign and future cells
float * cur_blk_bias = dst_bias + i*n_blocks;
for (int64_t b = 0; b < n_blocks; ++b) {
// finite, so it can never meet a -inf and produce a nan
cur_blk_bias[b] = b*r >= tail_start ? 1e9f : (filled[b] < r ? -INFINITY : 0.0f);
}
continue;
}
float * cur_bias = dst_bias + i*n_kv;
for (int64_t j = 0; j < n_kv; ++j) {
float v = -INFINITY;
if (!cells.is_empty(j) && cells.seq_has(j, seq_id) && cells.pos_get(j) <= q) {
// finite, so it can never meet a -inf and produce a nan
v = cells.pos_get(j) >= tail_start ? 1e9f : (blk_of[j] < 0 ? -INFINITY : 0.0f);
}
cur_bias[j] = v;
}
}
}
}
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include "llama-memory-hybrid.h"
#include <memory>
#include <vector>
//
// llama_memory_hybrid_idx
//
// llama_memory_hybrid plus a third cache with one indexer key per token, for block-sparse attention (qwen4exp QSA)
// the indexer is a side buffer over the attention cells: same size, padding, streams and slots, so cell j is one token in both
class llama_memory_hybrid_idx : public llama_memory_hybrid {
public:
llama_memory_hybrid_idx(
const llama_model & model,
/* attn */
ggml_type type_k,
ggml_type type_v,
bool v_trans,
uint32_t kv_size,
uint32_t n_pad,
uint32_t n_swa,
llama_swa_type swa_type,
/* recurrent */
ggml_type type_r,
ggml_type type_s,
uint32_t rs_size,
/* common */
uint32_t n_seq_max,
uint32_t n_rs_seq,
bool offload,
bool unified,
/* layer filters */
const layer_filter_cb & filter_attn,
const layer_filter_cb & filter_recr,
/* the indexer cache exists only if this is given */
const layer_filter_cb & filter_idx);
~llama_memory_hybrid_idx() = default;
//
// llama_memory_i
//
llama_memory_context_ptr init_batch(
llama_batch_allocr & balloc,
uint32_t n_ubatch,
bool embd_all) override;
llama_memory_context_ptr init_full() override;
llama_memory_context_ptr init_update(llama_context * lctx, bool optimize) override;
void clear(bool data) override;
bool seq_rm (llama_seq_id seq_id, llama_pos p0, llama_pos p1) override;
void seq_cp (llama_seq_id seq_id_src, llama_seq_id seq_id_dst, llama_pos p0, llama_pos p1) override;
void seq_keep(llama_seq_id seq_id) override;
void seq_add (llama_seq_id seq_id, llama_pos p0, llama_pos p1, llama_pos shift) override;
void seq_div (llama_seq_id seq_id, llama_pos p0, llama_pos p1, int d) override;
std::map<ggml_backend_buffer_type_t, size_t> memory_breakdown() const override;
// state write/load
void state_write(llama_io_write_i & io, llama_seq_id seq_id = -1, llama_state_seq_flags flags = 0) const override;
void state_read (llama_io_read_i & io, llama_seq_id seq_id = -1, llama_state_seq_flags flags = 0) override;
//
// llama_memory_hybrid_idx specific API
//
llama_kv_cache * get_mem_idx() const; // nullptr when the model carries no indexer
private:
// forget seq_id (all of it if seq_id < 0) in every cache at once, so a failed restore cannot leave the caches out of step
// seq_id < 0 drops the whole context, as the caches themselves do on a failed restore
void state_drop(llama_seq_id seq_id);
// the indexer cache holds one key head per layer, so it needs its own hparams:
// llama_kv_cache keeps a reference to what it is given
llama_hparams hparams_idx;
const std::unique_ptr<llama_kv_cache> mem_idx;
};
class llama_memory_hybrid_idx_context : public llama_memory_hybrid_context {
public:
using slot_info_vec_t = llama_kv_cache::slot_info_vec_t;
// used for errors
explicit llama_memory_hybrid_idx_context(llama_memory_status status);
// used to create a full-cache context
explicit llama_memory_hybrid_idx_context(llama_memory_hybrid_idx * mem);
// used to create an update context
llama_memory_hybrid_idx_context(
llama_memory_hybrid_idx * mem,
llama_context * lctx,
bool optimize);
// used to create a batch processing context from a batch
llama_memory_hybrid_idx_context(
llama_memory_hybrid_idx * mem,
slot_info_vec_t sinfos_attn,
slot_info_vec_t sinfos_idx,
std::vector<llama_ubatch> ubatches);
~llama_memory_hybrid_idx_context() = default;
//
// llama_memory_context_i
//
bool next() override;
bool apply() override;
//
// llama_memory_hybrid_idx_context specific API
//
// nullptr with no indexer, and for the update context, which builds no sparse graph
const llama_kv_cache_context * get_idx() const;
// streams in the current slot info, the `ns` of get_k/get_v; 1 if unified
uint32_t get_n_stream() const;
// block-compressed sparse attention (qwen4exp QSA) over the cells of the indexer cache.
// Blocks cut the position line, not the cell array, so no caller assumes a contiguous layout:
// cell_blk I32 [n_kv, ns] block each cell belongs to
// blk_cells I32 [ratio*n_blocks, ns] cells making up each block
// blk_pos I32 [4*n_blocks*ns] mrope position rows of each block's first token
// bias F32 [n_kv, n_tokens/ns, ns] -inf where invisible, large where always visible
// blk_bias asks for the bias per block instead: [n_blocks, n_tokens/ns, ns]
// the caller then adds the attention mask, the only part of the bias that varies within a block
void set_input_qsa(ggml_tensor * cell_blk, ggml_tensor * blk_cells, ggml_tensor * blk_pos,
ggml_tensor * bias, const llama_ubatch * ubatch, uint32_t ratio,
bool blk_bias) const;
private:
const llama_memory_hybrid_idx * mem = nullptr;
// streams per ubatch, read from the slot infos before ctx_idx takes them
// declared first, so it is initialised while sinfos_idx is still intact
const std::vector<uint32_t> ns_ubatch;
// null unless the model has an indexer and this is a batch or full context
const llama_memory_context_ptr ctx_idx;
// mirrors the base class's ubatch cursor, which is private there
size_t i_cur = 0;
};
+56 -4
View File
@@ -51,7 +51,8 @@ llama_memory_recurrent::llama_memory_recurrent(
auto it = ctx_map.find(buft);
if (it == ctx_map.end()) {
ggml_init_params params = {
/*.mem_size =*/ size_t(2u*n_layer*ggml_tensor_overhead()),
// r and s per layer, plus the separate PLE conv row where the model has one
/*.mem_size =*/ size_t((hparams.ple_conv_state() > 0 ? 3u : 2u)*n_layer*ggml_tensor_overhead()),
/*.mem_buffer =*/ NULL,
/*.no_alloc =*/ true,
};
@@ -71,6 +72,7 @@ llama_memory_recurrent::llama_memory_recurrent(
r_l.resize(n_layer);
s_l.resize(n_layer);
p_l.resize(n_layer);
for (int i = 0; i < n_layer; i++) {
if (filter && !filter(i)) {
@@ -103,6 +105,13 @@ llama_memory_recurrent::llama_memory_recurrent(
ggml_format_name(s, "cache_s_l%d", i);
r_l[i] = r;
s_l[i] = s;
// the PLE history needs its own row: Meta must mirror it while the delta-net conv state next door stays split
if (hparams.ple_conv_state() > 0 && hparams.is_ple(i)) {
ggml_tensor * p = ggml_new_tensor_2d(ctx, type_r, hparams.ple_conv_state(), n_rows);
ggml_format_name(p, "cache_ple_r_l%d", i);
p_l[i] = p;
}
}
// allocate tensors and initialize the buffers to avoid NaNs in the padding
@@ -119,11 +128,13 @@ llama_memory_recurrent::llama_memory_recurrent(
{
const size_t memory_size_r = size_r_bytes();
const size_t memory_size_s = size_s_bytes();
const size_t memory_size_p = size_p_bytes();
LLAMA_LOG_INFO("%s: size = %7.2f MiB (%6u cells, %3d layers, %2u seqs %2u rs_seq), R (%s): %7.2f MiB, S (%s): %7.2f MiB\n", __func__,
(float)(memory_size_r + memory_size_s) / (1024.0f * 1024.0f), mem_size, n_layer, n_seq_max, n_rs_seq,
LLAMA_LOG_INFO("%s: size = %7.2f MiB (%6u cells, %3d layers, %2u seqs %2u rs_seq), R (%s): %7.2f MiB, S (%s): %7.2f MiB, P (%s): %7.2f MiB\n", __func__,
(float)(memory_size_r + memory_size_s + memory_size_p) / (1024.0f * 1024.0f), mem_size, n_layer, n_seq_max, n_rs_seq,
ggml_type_name(type_r), (float)memory_size_r / (1024.0f * 1024.0f),
ggml_type_name(type_s), (float)memory_size_s / (1024.0f * 1024.0f));
ggml_type_name(type_s), (float)memory_size_s / (1024.0f * 1024.0f),
ggml_type_name(type_r), (float)memory_size_p / (1024.0f * 1024.0f));
}
}
@@ -740,6 +751,18 @@ size_t llama_memory_recurrent::size_s_bytes() const {
return size_s_bytes;
}
size_t llama_memory_recurrent::size_p_bytes() const {
size_t size_p_bytes = 0;
for (const auto & p : p_l) {
if (p != nullptr) {
size_p_bytes += ggml_nbytes(p);
}
}
return size_p_bytes;
}
void llama_memory_recurrent::state_write(llama_io_write_i & io, llama_seq_id seq_id, llama_state_seq_flags flags) const {
GGML_UNUSED(flags);
@@ -899,6 +922,17 @@ void llama_memory_recurrent::state_write_data(llama_io_write_i & io, const std::
const size_t buf_size = range_size * r_size_row;
io.write_tensor(r_l[il], range.first * r_size_row, buf_size);
}
// the PLE conv history is a second recurrent row, so it has to travel with the first
if (p_l[il] != nullptr) {
const uint64_t p_size_row = ggml_row_size(p_l[il]->type, hparams.ple_conv_state());
io.write(&p_size_row, sizeof(p_size_row));
for (const auto & range : cell_ranges) {
const size_t range_size = range.second - range.first;
io.write_tensor(p_l[il], range.first * p_size_row, range_size * p_size_row);
}
}
}
if (!s_trans) {
@@ -1097,6 +1131,20 @@ bool llama_memory_recurrent::state_read_data(llama_io_read_i & io, uint32_t cell
// Read and set the keys for the whole cell range
io.read_tensor(r_l[il], head * r_size_row, cell_count * r_size_row);
}
if (p_l[il] != nullptr) {
uint64_t p_size_row_ref;
io.read(&p_size_row_ref, sizeof(p_size_row_ref));
const size_t p_size_row = ggml_row_size(p_l[il]->type, hparams.ple_conv_state());
if (p_size_row != p_size_row_ref) {
LLAMA_LOG_ERROR("%s: mismatched ple row size (%zu != %zu, layer %d)\n", __func__, p_size_row, (size_t) p_size_row_ref, il);
return false;
}
if (cell_count) {
io.read_tensor(p_l[il], head * p_size_row, cell_count * p_size_row);
}
}
}
if (!s_trans) {
@@ -1251,6 +1299,10 @@ ggml_tensor * llama_memory_recurrent_context::get_s_l(int32_t il) const {
return mem->s_l[il];
}
ggml_tensor * llama_memory_recurrent_context::get_p_l(int32_t il) const {
return mem->p_l[il];
}
int32_t llama_memory_recurrent_context::s_copy(int i) const {
const uint32_t cell_idx = i + mem->head;
const int32_t src0 = mem->cells[cell_idx].src0;
+4
View File
@@ -111,6 +111,8 @@ public:
// per layer
std::vector<ggml_tensor *> r_l;
std::vector<ggml_tensor *> s_l;
// a second conv history that must stay replicated across devices, so it cannot share the r row
std::vector<ggml_tensor *> p_l;
private:
//const llama_model & model;
@@ -125,6 +127,7 @@ private:
size_t size_r_bytes() const;
size_t size_s_bytes() const;
size_t size_p_bytes() const;
void state_write_meta(llama_io_write_i & io, const std::vector<std::pair<uint32_t, uint32_t>> & cell_ranges, llama_seq_id seq_id = -1) const;
void state_write_data(llama_io_write_i & io, const std::vector<std::pair<uint32_t, uint32_t>> & cell_ranges) const;
@@ -170,6 +173,7 @@ public:
ggml_tensor * get_r_l(int32_t il) const;
ggml_tensor * get_s_l(int32_t il) const;
ggml_tensor * get_p_l(int32_t il) const;
int32_t s_copy(int i) const;
+59 -13
View File
@@ -438,11 +438,34 @@ void llama_file::write_u32(uint32_t val) const { pimpl->write_u32(val); }
// llama_mmap
#if defined(_POSIX_MAPPED_FILES) || defined(_WIN32)
// merge `ranges` and return their complement within [0, limit)
static llama_mmap::ranges ranges_complement(llama_mmap::ranges ranges, size_t limit) {
llama_mmap::ranges res;
std::sort(ranges.begin(), ranges.end());
size_t pos = 0;
for (const auto & range : ranges) {
const size_t beg = std::min(range.first, limit);
const size_t end = std::min(range.second, limit);
if (beg > pos) {
res.emplace_back(pos, beg);
}
pos = std::max(pos, end);
}
if (pos < limit) {
res.emplace_back(pos, limit);
}
return res;
}
#endif
struct llama_mmap::impl {
#ifdef _POSIX_MAPPED_FILES
std::vector<std::pair<size_t, size_t>> mapped_fragments;
impl(struct llama_file * file, size_t prefetch, bool numa) {
impl(struct llama_file * file, size_t prefetch, bool numa, const llama_mmap::ranges & lazy_ranges) {
size = file->size();
int fd = file->file_id();
int flags = MAP_SHARED;
@@ -452,18 +475,34 @@ struct llama_mmap::impl {
LLAMA_LOG_WARN("warning: posix_fadvise(.., POSIX_FADV_SEQUENTIAL) failed: %s\n",
strerror(errno));
}
if (prefetch) { flags |= MAP_POPULATE; }
// MAP_POPULATE would fault in the lazy ranges too
if (prefetch && lazy_ranges.empty()) { flags |= MAP_POPULATE; }
#endif
addr = mmap(NULL, file->size(), PROT_READ, flags, fd, 0);
if (addr == MAP_FAILED) {
throw std::runtime_error(format("mmap failed: %s", strerror(errno)));
}
if (prefetch > 0) {
if (posix_madvise(addr, std::min(file->size(), prefetch), POSIX_MADV_WILLNEED)) {
LLAMA_LOG_WARN("warning: posix_madvise(.., POSIX_MADV_WILLNEED) failed: %s\n",
strerror(errno));
// page-aligned madvise over [beg, end), clamped to the file
auto advise = [&](size_t beg, size_t end, int advice, const char * name) {
const size_t page_size = sysconf(_SC_PAGESIZE);
beg = beg & ~(page_size - 1);
end = std::min((end + page_size - 1) & ~(page_size - 1), file->size());
if (beg >= end) {
return;
}
if (posix_madvise((char *) addr + beg, end - beg, advice)) {
LLAMA_LOG_WARN("warning: posix_madvise(.., %s) failed: %s\n", name, strerror(errno));
}
};
if (prefetch > 0) {
for (const auto & range : ranges_complement(lazy_ranges, std::min(file->size(), prefetch))) {
advise(range.first, range.second, POSIX_MADV_WILLNEED, "POSIX_MADV_WILLNEED");
}
}
for (const auto & range : lazy_ranges) {
advise(range.first, range.second, POSIX_MADV_RANDOM, "POSIX_MADV_RANDOM");
}
if (numa) {
if (posix_madvise(addr, file->size(), POSIX_MADV_RANDOM)) {
@@ -533,7 +572,7 @@ struct llama_mmap::impl {
#elif defined(_WIN32)
HANDLE hMapping = nullptr;
impl(struct llama_file * file, size_t prefetch, bool numa) {
impl(struct llama_file * file, size_t prefetch, bool numa, const llama_mmap::ranges & lazy_ranges) {
GGML_UNUSED(numa);
size = file->size();
@@ -563,10 +602,15 @@ struct llama_mmap::impl {
pPrefetchVirtualMemory = (decltype(pPrefetchVirtualMemory))(void *) GetProcAddress(hKernel32, "PrefetchVirtualMemory");
if (pPrefetchVirtualMemory) {
WIN32_MEMORY_RANGE_ENTRY range;
range.VirtualAddress = addr;
range.NumberOfBytes = (SIZE_T) std::min(size, prefetch);
if (!pPrefetchVirtualMemory(GetCurrentProcess(), 1, &range, 0)) {
std::vector<WIN32_MEMORY_RANGE_ENTRY> entries;
for (const auto & range : ranges_complement(lazy_ranges, std::min(size, prefetch))) {
WIN32_MEMORY_RANGE_ENTRY entry;
entry.VirtualAddress = (char *) addr + range.first;
entry.NumberOfBytes = (SIZE_T) (range.second - range.first);
entries.push_back(entry);
}
if (!entries.empty() &&
!pPrefetchVirtualMemory(GetCurrentProcess(), (ULONG_PTR) entries.size(), entries.data(), 0)) {
LLAMA_LOG_WARN("warning: PrefetchVirtualMemory failed: %s\n",
llama_format_win_err(GetLastError()).c_str());
}
@@ -597,10 +641,11 @@ struct llama_mmap::impl {
}
}
#else
impl(struct llama_file * file, size_t prefetch, bool numa) {
impl(struct llama_file * file, size_t prefetch, bool numa, const llama_mmap::ranges & lazy_ranges) {
GGML_UNUSED(file);
GGML_UNUSED(prefetch);
GGML_UNUSED(numa);
GGML_UNUSED(lazy_ranges);
throw std::runtime_error("mmap not supported");
}
@@ -617,7 +662,8 @@ struct llama_mmap::impl {
size_t size;
};
llama_mmap::llama_mmap(struct llama_file * file, size_t prefetch, bool numa) : pimpl(std::make_unique<impl>(file, prefetch, numa)) {}
llama_mmap::llama_mmap(struct llama_file * file, size_t prefetch, bool numa,
const ranges & lazy_ranges) : pimpl(std::make_unique<impl>(file, prefetch, numa, lazy_ranges)) {}
llama_mmap::~llama_mmap() = default;
size_t llama_mmap::size() const { return pimpl->size; }
+6 -1
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>
#include <cstdio>
@@ -41,8 +42,12 @@ private:
};
struct llama_mmap {
// list of [first, last) byte ranges within a file
using ranges = std::vector<std::pair<size_t, size_t>>;
llama_mmap(const llama_mmap &) = delete;
llama_mmap(struct llama_file * file, size_t prefetch = (size_t) -1, bool numa = false);
llama_mmap(struct llama_file * file, size_t prefetch = (size_t) -1, bool numa = false,
const ranges & lazy_ranges = {});
~llama_mmap();
size_t size() const;
+39 -17
View File
@@ -321,10 +321,11 @@ namespace GGUFMeta {
case GGUF_TYPE_UINT32:
case GGUF_TYPE_INT32: type_ok = (std::is_same<T, int32_t>::value) ||
(std::is_same<T, uint32_t>::value); break;
case GGUF_TYPE_UINT64: type_ok = (std::is_same<T, uint64_t>::value); break;
case GGUF_TYPE_FLOAT32: type_ok = (std::is_same<T, float>::value); break;
case GGUF_TYPE_STRING: type_ok = (std::is_same<T, std::string>::value); break;
default:
throw std::runtime_error(format("%s is not a string/float32/uint32/int32 array", key.c_str()));
throw std::runtime_error(format("%s is not a string/float32/uint32/int32/uint64 array", key.c_str()));
}
if (!type_ok) {
throw std::runtime_error(format("%s has wrong array element type %s", key.c_str(), gguf_type_name(arr_info.gt)));
@@ -367,10 +368,11 @@ namespace GGUFMeta {
case GGUF_TYPE_UINT32:
case GGUF_TYPE_INT32: type_ok = (std::is_same<T, int32_t>::value) ||
(std::is_same<T, uint32_t>::value); break;
case GGUF_TYPE_UINT64: type_ok = (std::is_same<T, uint64_t>::value); break;
case GGUF_TYPE_FLOAT32: type_ok = (std::is_same<T, float>::value); break;
case GGUF_TYPE_STRING: type_ok = (std::is_same<T, std::string>::value); break;
default:
throw std::runtime_error(format("%s is not a string/float32/uint32/int32 array", key.c_str()));
throw std::runtime_error(format("%s is not a string/float32/uint32/int32/uint64 array", key.c_str()));
}
if (!type_ok) {
throw std::runtime_error(format("%s has wrong array element type %s", key.c_str(), gguf_type_name(arr_info.gt)));
@@ -410,6 +412,9 @@ namespace GGUFMeta {
template bool llama_model_loader::get_arr<std::array<int32_t, 512>>(enum llm_kv kid, std::array<int32_t, 512> & result, bool required);
template bool llama_model_loader::get_arr<std::vector<int32_t>>(enum llm_kv kid, std::vector<int32_t> & result, bool required);
template bool llama_model_loader::get_arr<std::array<uint32_t, LLAMA_MAX_LAYERS>>(enum llm_kv kid, std::array<uint32_t, LLAMA_MAX_LAYERS> & result, bool required);
template bool llama_model_loader::get_arr<std::vector<uint32_t>>(enum llm_kv kid, std::vector<uint32_t> & result, bool required);
template bool llama_model_loader::get_arr<std::array<uint64_t, LLAMA_MAX_PLE_NGRAM>>(enum llm_kv kid, std::array<uint64_t, LLAMA_MAX_PLE_NGRAM> & result, bool required);
template bool llama_model_loader::get_arr<std::array<uint64_t, LLAMA_MAX_PLE_HEADS>>(enum llm_kv kid, std::array<uint64_t, LLAMA_MAX_PLE_HEADS> & result, bool required);
template<typename T>
bool llama_model_loader::get_key(const std::string & key, T & result, bool required) {
@@ -1282,6 +1287,18 @@ struct ggml_tensor * llama_model_loader::create_tensor(
return NULL;
}
if ((flags & TENSOR_READ_LAZY) && use_mmap && tensor_read_lazy != LLAMA_TENSOR_READ_LAZY_OFF) {
// in auto mode, small tensors are cheap enough to keep resident
constexpr size_t auto_lazy_min_size = 4ull * 1024 * 1024 * 1024;
if (tensor_read_lazy == LLAMA_TENSOR_READ_LAZY_ON || ggml_nbytes(cur) > auto_lazy_min_size) {
const auto & w = require_weight(tn.str().c_str());
lazy_tensor_ranges[w.idx].emplace_back(w.offs, w.offs + ggml_nbytes(cur));
LLAMA_LOG_INFO("%s: tensor %s (size = %zu MiB) lazy read enabled\n",
__func__, tn.str().c_str(), ggml_nbytes(cur)/1024/1024);
}
}
ggml_tensor t_meta = *cur;
if (flags & TENSOR_ALLOW_RESHAPE) {
for (size_t dim = 0; dim < GGML_MAX_DIMS; dim++) {
@@ -1349,7 +1366,9 @@ void llama_model_loader::init_mappings(bool prefetch, llama_mlocks * mlock_mmaps
if (use_mmap) {
mappings.reserve(files.size());
mmaps_used.reserve(files.size());
for (const auto & file : files) {
for (uint32_t idx = 0; idx < files.size(); idx++) {
const auto & file = files[idx];
bool is_numa = false;
auto * dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
@@ -1361,7 +1380,11 @@ void llama_model_loader::init_mappings(bool prefetch, llama_mlocks * mlock_mmaps
}
}
std::unique_ptr<llama_mmap> mapping = std::make_unique<llama_mmap>(file.get(), prefetch ? -1 : 0, is_numa);
const auto it_lazy = lazy_tensor_ranges.find(idx);
static const llama_mmap::ranges no_lazy_ranges;
std::unique_ptr<llama_mmap> mapping = std::make_unique<llama_mmap>(file.get(), prefetch ? -1 : 0, is_numa,
it_lazy != lazy_tensor_ranges.end() ? it_lazy->second : no_lazy_ranges);
mmaps_used.emplace_back(mapping->size(), 0);
if (mlock_mmaps) {
std::unique_ptr<llama_mlock> mlock_mmap(new llama_mlock());
@@ -1400,27 +1423,26 @@ void llama_model_loader::unmap_weight(const llama_tensor_weight & w) const {
mappings.at(w.idx)->unmap_fragment(w.offs, w.offs + ggml_nbytes(w.tensor));
}
void llama_model_loader::load_data_for(struct ggml_tensor * cur) const {
const auto & w = require_weight(ggml_get_name(cur));
const void * llama_model_loader::load_data_range(const llama_tensor_weight & w, size_t offs, size_t size, void * buf) const {
GGML_ASSERT(offs + size <= ggml_nbytes(w.tensor));
const void * data = buf;
if (use_mmap) {
const auto & mapping = mappings.at(w.idx);
if (cur->data == nullptr) {
cur->data = (uint8_t *)mapping->addr() + w.offs;
} else {
memcpy(cur->data, (uint8_t *)mapping->addr() + w.offs, ggml_nbytes(cur));
}
data = (const uint8_t *) mappings.at(w.idx)->addr() + w.offs + offs;
} else {
GGML_ASSERT(cur->data != nullptr);
GGML_ASSERT(buf != nullptr);
GGML_ASSERT(w.idx < files.size());
const auto & file = files.at(w.idx);
file->seek(w.offs, SEEK_SET);
file->read_raw(cur->data, ggml_nbytes(cur));
file->seek(w.offs + offs, SEEK_SET);
file->read_raw(buf, size);
}
if (check_tensors && !ggml_validate_row_data(cur->type, cur->data, ggml_nbytes(cur))) {
throw std::runtime_error(format("tensor '%s' has invalid data", ggml_get_name(cur)));
if (check_tensors && !ggml_validate_row_data(w.tensor->type, data, size)) {
throw std::runtime_error(format("tensor '%s' has invalid data", ggml_get_name(w.tensor)));
}
return data;
}
bool llama_model_loader::load_all_data(
+10 -2
View File
@@ -68,6 +68,7 @@ struct llama_model_loader {
static const int TENSOR_SKIP = 1 << 2;
static const int TENSOR_SKIP_IF_VIRTUAL = 1 << 3;
static const int TENSOR_ALLOW_RESHAPE = 1 << 4;
static const int TENSOR_READ_LAZY = 1 << 5; // read rows on demand instead of loading whole tensor; requires mmap for now
int n_kv = 0;
int n_tensors = 0;
@@ -82,12 +83,18 @@ struct llama_model_loader {
bool no_alloc;
bool load_mtp;
// set by the caller before the create_tensor() calls
enum llama_tensor_read_lazy tensor_read_lazy = LLAMA_TENSOR_READ_LAZY_OFF;
llama_files files;
llama_ftype ftype;
llama_fver fver;
llama_mmaps mappings;
// byte ranges of TENSOR_READ_LAZY tensors, per file index
std::map<uint32_t, llama_mmap::ranges> lazy_tensor_ranges;
std::map<std::string, llama_tensor_weight, weight_name_comparer> weights_map;
std::unordered_map<std::string, llama_model_kv_override> kv_overrides;
const llama_model_tensor_buft_override * tensor_buft_overrides;
@@ -197,8 +204,9 @@ struct llama_model_loader {
// release a weight's mmap pages
void unmap_weight(const llama_tensor_weight & w) const;
// for backwards compatibility, does not support ggml-backend
void load_data_for(struct ggml_tensor * cur) const;
// read a byte range of a weight's data
// with mmap, returns a pointer into the mapping, otherwise reads into buf and returns buf
const void * load_data_range(const llama_tensor_weight & w, size_t offs, size_t size, void * buf) const;
// Returns false if cancelled by progress_callback
bool load_all_data(
+37
View File
@@ -60,6 +60,10 @@ void llama_model_saver::add_kv(const enum llm_kv key, const int32_t value) {
gguf_set_val_i32(gguf_ctx, llm_kv(key).c_str(), value);
}
void llama_model_saver::add_kv(const enum llm_kv key, const uint64_t value) {
gguf_set_val_u64(gguf_ctx, llm_kv(key).c_str(), value);
}
void llama_model_saver::add_kv(const enum llm_kv key, const float value) {
gguf_set_val_f32(gguf_ctx, llm_kv(key).c_str(), value);
}
@@ -113,6 +117,8 @@ void llama_model_saver::add_kv(const enum llm_kv key, const Container & value, c
gguf_set_arr_data(gguf_ctx, llm_kv(key).c_str(), GGUF_TYPE_BOOL, value.data(), n_values);
} else if (std::is_same<typename Container::value_type, int32_t>::value) {
gguf_set_arr_data(gguf_ctx, llm_kv(key).c_str(), GGUF_TYPE_INT32, value.data(), n_values);
} else if (std::is_same<typename Container::value_type, uint64_t>::value) {
gguf_set_arr_data(gguf_ctx, llm_kv(key).c_str(), GGUF_TYPE_UINT64, value.data(), n_values);
} else if (std::is_same<typename Container::value_type, float>::value) {
gguf_set_arr_data(gguf_ctx, llm_kv(key).c_str(), GGUF_TYPE_FLOAT32, value.data(), n_values);
} else if (std::is_same<Container, std::string>::value) {
@@ -124,6 +130,7 @@ void llama_model_saver::add_kv(const enum llm_kv key, const Container & value, c
// instantiate for external usage:
template void llama_model_saver::add_kv<std::vector<uint32_t>>(const enum llm_kv, const std::vector<uint32_t> &, const bool);
template void llama_model_saver::add_kv<std::vector<float>>(const enum llm_kv, const std::vector<float> &, const bool);
template void llama_model_saver::add_kv<std::vector<uint64_t>>(const enum llm_kv, const std::vector<uint64_t> &, const bool);
void llama_model_saver::add_kv(const enum llm_kv key, const std::vector<std::string> & value) {
std::vector<const char *> tmp(value.size());
@@ -308,6 +315,32 @@ void llama_model_saver::add_kv_from_model() {
add_kv(LLM_KV_HYPER_CONNECTION_SINKHORN_ITERATIONS, hparams.dsv4_hc_sinkhorn_iters);
add_kv(LLM_KV_HYPER_CONNECTION_EPSILON, hparams.dsv4_hc_eps);
add_kv(LLM_KV_HASH_LAYER_COUNT, hparams.dsv4_hash_layer_count);
add_kv(LLM_KV_HYPER_CONNECTION_LOW_RANK, hparams.hc_low_rank);
// the PLE group only means anything whole: write all of it or none
if (hparams.ple_n_heads > 0) {
std::vector<uint32_t> ple_layers;
for (uint32_t il = 0; il < hparams.n_layer_all; ++il) {
if (hparams.is_ple_impl[il]) {
ple_layers.push_back(il);
}
}
add_kv(LLM_KV_PLE_LAYERS, ple_layers);
add_kv(LLM_KV_PLE_NGRAM_SIZE, hparams.ple_ngram_size);
add_kv(LLM_KV_PLE_HEADS_PER_NGRAM, hparams.ple_heads_per_ngram);
add_kv(LLM_KV_PLE_CONV_KERNEL, hparams.ple_conv_kernel);
add_kv(LLM_KV_PLE_EOS_TOKEN_ID, hparams.ple_eos_token_id);
add_kv(LLM_KV_EMBEDDING_LENGTH_PER_LAYER, hparams.ple_head_dim);
add_kv(LLM_KV_PLE_LAYER_MULTIPLIERS, std::vector<uint64_t>(
hparams.ple_layer_multipliers.begin(),
hparams.ple_layer_multipliers.begin() + hparams.ple_ngram_size));
add_kv(LLM_KV_PLE_HEAD_OFFSETS, std::vector<uint64_t>(
hparams.ple_head_offsets.begin(),
hparams.ple_head_offsets.begin() + hparams.ple_n_heads));
add_kv(LLM_KV_PLE_HEAD_VOCAB_SIZES, std::vector<uint64_t>(
hparams.ple_head_vocab_sizes.begin(),
hparams.ple_head_vocab_sizes.begin() + hparams.ple_n_heads));
}
const float rope_scaling_factor = hparams.rope_freq_scale_train == 1.0f ? 0.0f : 1.0f/hparams.rope_freq_scale_train;
@@ -442,6 +475,10 @@ void llama_model_saver::add_tensors_from_model() {
add_tensor(model->hc_head_fn);
add_tensor(model->hc_head_base);
add_tensor(model->hc_head_scale);
add_tensor(model->per_layer_tok_embd);
add_tensor(model->hc_head_norm);
add_tensor(model->hc_head_down);
add_tensor(model->hc_head_up);
for (const struct llama_layer & layer : model->layers) {
for (size_t i = 0; i < sizeof(layer)/sizeof(struct ggml_tensor *); ++i) {
+1
View File
@@ -21,6 +21,7 @@ struct llama_model_saver {
void add_kv(enum llm_kv key, uint32_t value);
void add_kv(enum llm_kv key, int32_t value);
void add_kv(enum llm_kv key, uint64_t value);
void add_kv(enum llm_kv key, float value);
void add_kv(enum llm_kv key, bool value);
void add_kv(enum llm_kv key, const char * value);
+65 -4
View File
@@ -16,6 +16,7 @@
#include "llama-kv-cache-dsv4.h"
#include "llama-memory-hybrid.h"
#include "llama-memory-hybrid-iswa.h"
#include "llama-memory-hybrid-idx.h"
#include "llama-memory-recurrent.h"
#include "llama.h"
@@ -319,6 +320,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
return new llama_model_qwen35(params);
case LLM_ARCH_QWEN35MOE:
return new llama_model_qwen35moe(params);
case LLM_ARCH_QWEN4EXP:
return new llama_model_qwen4exp(params);
case LLM_ARCH_MISTRAL3:
return new llama_model_mistral3(params);
case LLM_ARCH_EAGLE3:
@@ -376,6 +379,7 @@ 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_idx_cache ("cache_idx_(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");
@@ -391,6 +395,7 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
static const std::regex pattern_ssm_beta ("blk\\.\\d*\\.ssm_beta.weight");
static const std::regex pattern_ssm_beta_alpha ("blk\\.\\d*\\.ssm_ba.weight");
static const std::regex pattern_r_cache ("cache_r_l\\d*");
static const std::regex pattern_ple_r_cache ("cache_ple_r_l\\d*");
static const std::regex pattern_s_cache ("cache_s_l\\d*");
static const std::regex pattern_ssm_conv1d ("blk\\.\\d*\\.ssm_conv1d.weight");
static const std::regex pattern_ssm_out_weight ("blk\\.\\d*\\.ssm_out.weight");
@@ -488,6 +493,16 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
}
}
// the qsa indexer has one key head and its projections are mirrored, so its cache cannot be split
if (std::regex_match(tensor_name, pattern_idx_cache)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED);
}
// the PLE table is model-level and its conv is mirrored, so every device runs the whole conv and needs the whole history
if (std::regex_match(tensor_name, pattern_ple_r_cache)) {
return get_tensor_config_impl(GGML_BACKEND_SPLIT_AXIS_MIRRORED);
}
// 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");
@@ -576,7 +591,8 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
};
auto get_split_segments = [&](int axis, uint32_t il) -> std::vector<std::pair<int64_t, uint32_t>> {
if (ud->model->arch == LLM_ARCH_QWEN3NEXT || ud->model->arch == LLM_ARCH_QWEN35 || ud->model->arch == LLM_ARCH_QWEN35MOE) {
if (ud->model->arch == LLM_ARCH_QWEN3NEXT || ud->model->arch == LLM_ARCH_QWEN35 || ud->model->arch == LLM_ARCH_QWEN35MOE ||
ud->model->arch == LLM_ARCH_QWEN4EXP) {
const int64_t head_k_dim = hparams.ssm_d_state;
const int64_t head_v_dim = hparams.ssm_d_state;
const int64_t n_k_heads = hparams.ssm_n_group;
@@ -714,7 +730,8 @@ struct ggml_backend_meta_split_state llama_meta_device_get_split_state(const str
if (std::regex_match(tensor_name, pattern_q_weight) || std::regex_match(tensor_name, pattern_q_bias)) {
GGML_ASSERT(segments.size() == 1);
// some models have Q gate tensors, for those cases the granularity needs to be doubled:
if (ud->model->arch == LLM_ARCH_QWEN3NEXT || ud->model->arch == LLM_ARCH_QWEN35 || ud->model->arch == LLM_ARCH_QWEN35MOE) {
if (ud->model->arch == LLM_ARCH_QWEN3NEXT || ud->model->arch == LLM_ARCH_QWEN35 || ud->model->arch == LLM_ARCH_QWEN35MOE ||
ud->model->arch == LLM_ARCH_QWEN4EXP) {
return {std::lcm(2*n_embd_q, blck_size_perf)};
}
return {granularity_q};
@@ -927,6 +944,7 @@ const char * llm_type_name(llm_type type) {
case LLM_TYPE_35B_A3B: return "35B.A3B";
case LLM_TYPE_48B_A3B: return "48B.A3B";
case LLM_TYPE_80B_A3B: return "80B.A3B";
case LLM_TYPE_A3B: return "A3B";
case LLM_TYPE_100B_A6B: return "100B.A6B";
case LLM_TYPE_102B_A12B: return "102B.A12B";
case LLM_TYPE_106B_A12B: return "106B.A12B";
@@ -2431,6 +2449,10 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
// layer filters, so pick the right one here
llama_memory_hybrid::layer_filter_cb filter_attn = nullptr;
llama_memory_hybrid::layer_filter_cb filter_recr = nullptr;
// only the sparse-attention architectures use llama_memory_hybrid_idx
// a null filter_idx means the GGUF has no indexer tensors
llama_memory_hybrid::layer_filter_cb filter_idx = nullptr;
const bool needs_mem_idx = (arch == LLM_ARCH_QWEN4EXP);
if (arch == LLM_ARCH_FALCON_H1) {
filter_attn = [&](uint32_t) { return true; };
filter_recr = [&](uint32_t) { return true; };
@@ -2441,13 +2463,20 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
filter_recr = [&](uint32_t il) {
return hparams.is_recr(il) && hparams.n_ff(il) == 0;
};
} else if (arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || arch == LLM_ARCH_MINIMAX_01) {
} else if (arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || arch == LLM_ARCH_QWEN4EXP || arch == LLM_ARCH_MINIMAX_01) {
filter_attn = [&](uint32_t il) {
return il < hparams.n_layer() && !hparams.is_recr(il);
};
filter_recr = [&](uint32_t il) {
return il < hparams.n_layer() && hparams.is_recr(il);
};
if (arch == LLM_ARCH_QWEN4EXP && hparams.indexer_head_size > 0) {
// QSA runs on the dense-attention layers only
filter_idx = [&](uint32_t il) {
return il < hparams.n_layer() && !hparams.is_recr(il);
};
}
}
if (hparams.swa_type != LLAMA_SWA_TYPE_NONE) {
@@ -2470,6 +2499,27 @@ llama_memory_i * llama_model::create_memory(const llama_memory_params & params,
/* unified */ cparams.kv_unified,
/* filter_attn */ std::move(filter_attn),
/* filter_recr */ std::move(filter_recr));
} else if (needs_mem_idx) {
// sparse attention over a per-token indexer cache, in its own memory type
res = new llama_memory_hybrid_idx(
/* model */ *this,
/* attn_type_k */ params.type_k,
/* attn_type_v */ params.type_v,
/* attn_v_trans */ !cparams.flash_attn,
/* attn_kv_size */ cparams.n_ctx_seq,
/* attn_n_pad */ 1,
/* attn_n_swa */ hparams.n_swa,
/* attn_swa_type */ hparams.swa_type,
/* recurrent_type_k */ GGML_TYPE_F32,
/* recurrent_type_v */ GGML_TYPE_F32,
/* recurrent_kv_size */ std::max((uint32_t) 1, cparams.n_seq_max),
/* n_seq_max */ cparams.n_seq_max,
/* n_rs_seq */ cparams.n_rs_seq,
/* offload */ cparams.offload_kqv,
/* unified */ cparams.kv_unified,
/* filter_attn */ std::move(filter_attn),
/* filter_recr */ std::move(filter_recr),
/* filter_idx */ std::move(filter_idx));
} else {
res = new llama_memory_hybrid(
/* model */ *this,
@@ -2631,6 +2681,7 @@ llama_model_params llama_model_default_params() {
/*.n_gpu_layers =*/ -1,
/*.split_mode =*/ LLAMA_SPLIT_MODE_LAYER,
/*.load_mode =*/ LLAMA_LOAD_MODE_AUTO,
/*.tensor_read_lazy =*/ LLAMA_TENSOR_READ_LAZY_AUTO,
/*.main_gpu =*/ 0,
/*.tensor_split =*/ nullptr,
/*.progress_callback =*/ nullptr,
@@ -2683,6 +2734,10 @@ int32_t llama_model_n_layer_nextn(const llama_model * model) {
return model->hparams.n_layer_nextn;
}
int32_t llama_model_dflash_selector_top_k(const llama_model * model) {
return model->hparams.dflash_selector_top_k;
}
int32_t llama_model_n_head(const llama_model * model) {
return model->hparams.n_head();
}
@@ -2881,6 +2936,10 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
return LLAMA_ROPE_TYPE_NEOX;
case LLM_ARCH_DFLASH:
// drafts for M-RoPE targets carry rope sections and follow the target's temporal dim
if (const auto & s = model->hparams.rope_sections; s[0] || s[1] || s[2] || s[3]) {
return LLAMA_ROPE_TYPE_MROPE;
}
// DSV4 DSpark drafters use DeepSeek-V4's normal RoPE; legacy DFlash backbones are NeoX
return model->hparams.dsv4_hc_mult > 0 ? LLAMA_ROPE_TYPE_NORM : LLAMA_ROPE_TYPE_NEOX;
@@ -2891,6 +2950,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
case LLM_ARCH_QWEN3VLMOE:
case LLM_ARCH_QWEN35:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_QWEN4EXP:
case LLM_ARCH_QWEN3TTS:
return LLAMA_ROPE_TYPE_IMROPE;
@@ -3067,7 +3127,8 @@ llama_model_base::llama_model_base(const struct llama_model_params & params) : l
TENSOR_NOT_REQUIRED (llama_model_loader::TENSOR_NOT_REQUIRED),
TENSOR_SKIP (llama_model_loader::TENSOR_SKIP),
TENSOR_SKIP_IF_VIRTUAL(llama_model_loader::TENSOR_SKIP_IF_VIRTUAL),
TENSOR_ALLOW_RESHAPE (llama_model_loader::TENSOR_ALLOW_RESHAPE) {}
TENSOR_ALLOW_RESHAPE (llama_model_loader::TENSOR_ALLOW_RESHAPE),
TENSOR_READ_LAZY (llama_model_loader::TENSOR_READ_LAZY) {}
ggml_tensor * llama_model_base::create_tensor(const LLM_TN_IMPL & tn, const std::initializer_list<int64_t> & ne, int flags) {
GGML_ASSERT(ml != nullptr);
+32
View File
@@ -129,6 +129,7 @@ enum llm_type {
LLM_TYPE_35B_A3B, // Qwen3.5
LLM_TYPE_48B_A3B, // Kimi Linear
LLM_TYPE_80B_A3B, // Qwen3 Next
LLM_TYPE_A3B, // Qwen3.8 Flash Next
LLM_TYPE_100B_A6B,
LLM_TYPE_102B_A12B, // Solar-Open
LLM_TYPE_106B_A12B, // GLM-4.5-Air
@@ -363,6 +364,11 @@ struct llama_layer {
struct ggml_tensor * ffn_exp_probs_b = nullptr;
struct ggml_tensor * ffn_gate_tid2eid = nullptr;
struct ggml_tensor * dflash_attn_conv_base = nullptr;
struct ggml_tensor * dflash_attn_conv_proj = nullptr;
struct ggml_tensor * dflash_ffn_conv_base = nullptr;
struct ggml_tensor * dflash_ffn_conv_proj = nullptr;
// mamba proj
struct ggml_tensor * ssm_in = nullptr;
struct ggml_tensor * ssm_x = nullptr;
@@ -555,6 +561,22 @@ struct llama_layer {
struct ggml_tensor * index_q_norm = nullptr;
struct ggml_tensor * index_k_norm = nullptr;
struct ggml_tensor * hc_attn_norm = nullptr;
struct ggml_tensor * hc_attn_down = nullptr;
struct ggml_tensor * hc_attn_up = nullptr;
struct ggml_tensor * hc_attn_inject = nullptr;
struct ggml_tensor * hc_ffn_norm = nullptr;
struct ggml_tensor * hc_ffn_down = nullptr;
struct ggml_tensor * hc_ffn_up = nullptr;
struct ggml_tensor * hc_ffn_inject = nullptr;
struct ggml_tensor * ple_key = nullptr;
struct ggml_tensor * ple_value = nullptr;
struct ggml_tensor * ple_norm_key = nullptr;
struct ggml_tensor * ple_norm_query = nullptr;
struct ggml_tensor * ple_norm_conv = nullptr;
struct ggml_tensor * ple_conv1d = nullptr;
// gemma4 layer output scale, reused for talkie embedding skip scale
struct ggml_tensor * out_scale = nullptr;
@@ -635,6 +657,10 @@ struct llama_model {
struct ggml_tensor * altup_proj = nullptr;
struct ggml_tensor * altup_unembd_proj = nullptr;
struct ggml_tensor * per_layer_tok_embd = nullptr;
struct ggml_tensor * hc_head_norm = nullptr;
struct ggml_tensor * hc_head_down = nullptr;
struct ggml_tensor * hc_head_up = nullptr;
struct ggml_tensor * per_layer_model_proj = nullptr;
struct ggml_tensor * per_layer_proj_norm = nullptr;
@@ -646,9 +672,14 @@ struct llama_model {
// dspark
struct ggml_tensor * dspark_markov_w1 = nullptr;
struct ggml_tensor * dspark_markov_w2 = nullptr;
struct ggml_tensor * dspark_markov_w2_s = nullptr;
struct ggml_tensor * dspark_conf_proj = nullptr;
struct ggml_tensor * dspark_conf_proj_b = nullptr;
struct ggml_tensor * dflash_selector_prev = nullptr;
struct ggml_tensor * dflash_selector_next = nullptr;
struct ggml_tensor * dflash_selector_hidden = nullptr;
// unified vector to store target-model extracted layer ids in eagle3, dflash, etc.
std::vector<int32_t> target_layer_ids;
@@ -756,6 +787,7 @@ struct llama_model_base : public llama_model {
const int TENSOR_SKIP;
const int TENSOR_SKIP_IF_VIRTUAL;
const int TENSOR_ALLOW_RESHAPE;
const int TENSOR_READ_LAZY;
explicit llama_model_base(const llama_model_params & params);
virtual ~llama_model_base() = default;
+102 -63
View File
@@ -38,6 +38,9 @@ enum class tensor_category {
OTHER
};
// max amount of tensor data kept in memory while quantizing a single tensor
static const size_t LLAMA_QUANT_MAX_BUF_SIZE = 8ull*1024*1024*1024;
static void zeros(std::ofstream & file, size_t n) {
char zero = 0;
for (size_t i = 0; i < n; ++i) {
@@ -211,31 +214,26 @@ struct tensor_metadata {
//
static void llama_tensor_dequantize_impl(
ggml_tensor * tensor, std::vector<no_init<float>> & output, std::vector<std::thread> & workers,
ggml_type type, const void * data, float * f32_output, std::vector<std::thread> & workers,
const size_t nelements, const int nthread
) {
if (output.size() < nelements) {
output.resize(nelements);
}
float * f32_output = (float *) output.data();
const ggml_type_traits * qtype = ggml_get_type_traits(tensor->type);
if (ggml_is_quantized(tensor->type)) {
const ggml_type_traits * qtype = ggml_get_type_traits(type);
if (ggml_is_quantized(type)) {
if (qtype->to_float == NULL) {
throw std::runtime_error(format("type %s unsupported for integer quantization: no dequantization available", ggml_type_name(tensor->type)));
throw std::runtime_error(format("type %s unsupported for integer quantization: no dequantization available", ggml_type_name(type)));
}
} else if (tensor->type != GGML_TYPE_F16 &&
tensor->type != GGML_TYPE_BF16) {
throw std::runtime_error(format("cannot dequantize/convert tensor type %s", ggml_type_name(tensor->type)));
} else if (type != GGML_TYPE_F16 &&
type != GGML_TYPE_BF16) {
throw std::runtime_error(format("cannot dequantize/convert tensor type %s", ggml_type_name(type)));
}
if (nthread < 2) {
if (tensor->type == GGML_TYPE_F16) {
ggml_fp16_to_fp32_row((ggml_fp16_t *)tensor->data, f32_output, nelements);
} else if (tensor->type == GGML_TYPE_BF16) {
ggml_bf16_to_fp32_row((ggml_bf16_t *)tensor->data, f32_output, nelements);
} else if (ggml_is_quantized(tensor->type)) {
qtype->to_float(tensor->data, f32_output, nelements);
if (type == GGML_TYPE_F16) {
ggml_fp16_to_fp32_row((const ggml_fp16_t *)data, f32_output, nelements);
} else if (type == GGML_TYPE_BF16) {
ggml_bf16_to_fp32_row((const ggml_bf16_t *)data, f32_output, nelements);
} else if (ggml_is_quantized(type)) {
qtype->to_float(data, f32_output, nelements);
} else {
GGML_ABORT("fatal error"); // unreachable
}
@@ -243,14 +241,14 @@ static void llama_tensor_dequantize_impl(
}
size_t block_size;
if (tensor->type == GGML_TYPE_F16 ||
tensor->type == GGML_TYPE_BF16) {
if (type == GGML_TYPE_F16 ||
type == GGML_TYPE_BF16) {
block_size = 1;
} else {
block_size = (size_t)ggml_blck_size(tensor->type);
block_size = (size_t)ggml_blck_size(type);
}
size_t block_size_bytes = ggml_type_size(tensor->type);
size_t block_size_bytes = ggml_type_size(type);
GGML_ASSERT(nelements % block_size == 0);
size_t nblocks = nelements / block_size;
@@ -265,16 +263,16 @@ static void llama_tensor_dequantize_impl(
size_t thr_elems = thr_blocks * block_size; // number of elements for this thread
size_t thr_block_bytes = thr_blocks * block_size_bytes; // number of input bytes for this thread
auto compute = [qtype] (ggml_type typ, uint8_t * inbuf, float * outbuf, int nels) {
auto compute = [qtype] (ggml_type typ, const uint8_t * inbuf, float * outbuf, int nels) {
if (typ == GGML_TYPE_F16) {
ggml_fp16_to_fp32_row((ggml_fp16_t *)inbuf, outbuf, nels);
ggml_fp16_to_fp32_row((const ggml_fp16_t *)inbuf, outbuf, nels);
} else if (typ == GGML_TYPE_BF16) {
ggml_bf16_to_fp32_row((ggml_bf16_t *)inbuf, outbuf, nels);
ggml_bf16_to_fp32_row((const ggml_bf16_t *)inbuf, outbuf, nels);
} else {
qtype->to_float(inbuf, outbuf, nels);
}
};
workers.emplace_back(compute, tensor->type, (uint8_t *) tensor->data + in_buff_offs, f32_output + out_buff_offs, thr_elems);
workers.emplace_back(compute, type, (const uint8_t *) data + in_buff_offs, f32_output + out_buff_offs, thr_elems);
in_buff_offs += thr_block_bytes;
out_buff_offs += thr_elems;
}
@@ -401,6 +399,12 @@ static ggml_type tensor_type_fallback(quantize_state_impl & qs, const ggml_tenso
case GGML_TYPE_Q5_K: return_type = GGML_TYPE_Q5_1; break;
case GGML_TYPE_Q6_K: return_type = GGML_TYPE_Q8_0; break;
default:
if (qk_k <= 32) {
// the target is already a 32-block type, so there is no smaller block to demote to
// the check below turns it into F16, as a 256-block type does when its fallback does not fit
return_type = target_type;
break;
}
throw std::runtime_error(format("no tensor type fallback is defined for type %s",
ggml_type_name(target_type)));
}
@@ -681,7 +685,21 @@ static ggml_type llama_tensor_get_type(quantize_state_impl & qs, const llama_mod
return tensor->type;
}
if (params->token_embedding_type < GGML_TYPE_COUNT && tm.category == tensor_category::TOKEN_EMBD) {
return params->token_embedding_type;
// per_layer_token_embd follows --token-embedding-type by default, but it is a large
// separate table, so let an explicit --tensor-type name it
bool named = false;
if (std::strcmp(tensor->name, "per_layer_token_embd.weight") == 0) {
const std::string tensor_name(tensor->name);
for (const auto & [pattern, qtype] : qs.tensor_type_patterns) {
if (std::regex_search(tensor_name, pattern)) {
named = true;
break;
}
}
}
if (!named) {
return params->token_embedding_type;
}
}
if (params->output_tensor_type < GGML_TYPE_COUNT && tm.category == tensor_category::OUTPUT) {
return params->output_tensor_type;
@@ -1093,6 +1111,8 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
std::vector<no_init<uint8_t>> work;
std::vector<no_init<float>> f32_conv_buf;
const size_t max_buf_size = params->max_buf_size ? params->max_buf_size : LLAMA_QUANT_MAX_BUF_SIZE;
int cur_split = -1;
std::ofstream fout;
auto close_ofstream = [&]() {
@@ -1143,15 +1163,13 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
const size_t tensor_size = ggml_nbytes(tensor);
if (!params->dry_run) {
if (!ml.use_mmap) {
if (read_data.size() < tensor_size) {
read_data.resize(tensor_size);
}
tensor->data = read_data.data();
// read a byte range of the current tensor
auto load_range = [&](size_t offs, size_t size) -> const void * {
if (!ml.use_mmap && read_data.size() < size) {
read_data.resize(size);
}
ml.load_data_for(tensor);
}
return ml.load_data_range(weight, offs, size, read_data.data());
};
LLAMA_LOG_INFO("[%4d/%4d] %-36s - [%s], type = %6s, ",
++idx, ml.n_tensors,
@@ -1166,7 +1184,6 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
// in then there's nothing to do.
bool quantize = cur_type != new_type;
void * new_data;
size_t new_size;
if (params->dry_run) {
@@ -1190,12 +1207,18 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
} else {
// no --dry-run, perform quantization
if (!quantize) {
new_data = tensor->data;
new_size = tensor_size;
LLAMA_LOG_INFO("size = %8.3f MiB\n", tensor_size/1024.0/1024.0);
} else {
const int64_t nelements = ggml_nelements(tensor);
// copy in slabs of whole rows, so that each slab can be validated
const size_t row_size = ggml_row_size(tensor->type, tensor->ne[0]);
const size_t slab_size = std::max<size_t>(row_size, (max_buf_size/row_size)*row_size);
for (size_t offs = 0; offs < tensor_size; offs += slab_size) {
const size_t size = std::min(slab_size, tensor_size - offs);
fout.write((const char *) load_range(offs, size), size);
}
} else {
const float * imatrix = nullptr;
if (imatrix_data) {
auto it = imatrix_data->find(tm.remapped_imatrix_name);
@@ -1227,43 +1250,60 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
throw std::runtime_error(format("Missing importance matrix for tensor %s in a very low-bit quantization", tensor->name));
}
float * f32_data;
if (tensor->type == GGML_TYPE_F32) {
f32_data = (float *) tensor->data;
} else if (ggml_is_quantized(tensor->type) && !params->allow_requantize) {
if (ggml_is_quantized(tensor->type) && !params->allow_requantize) {
throw std::runtime_error(format("requantizing from type %s is disabled", ggml_type_name(tensor->type)));
} else {
llama_tensor_dequantize_impl(tensor, f32_conv_buf, workers, nelements, nthread);
f32_data = (float *) f32_conv_buf.data();
}
LLAMA_LOG_INFO("converting to %s .. ", ggml_type_name(new_type));
fflush(stdout);
if (work.size() < (size_t)nelements * 4) {
work.resize(nelements * 4); // upper bound on size
}
new_data = work.data();
const int64_t n_per_row = tensor->ne[0];
const int64_t nrows = tensor->ne[1];
const size_t row_size_src = ggml_row_size(tensor->type, n_per_row);
const size_t row_size_dst = ggml_row_size(new_type, n_per_row);
// process the rows in slabs, so that the buffers stay below max_buf_size
const size_t bytes_per_row = row_size_src + row_size_dst + (tensor->type == GGML_TYPE_F32 ? 0 : n_per_row*sizeof(float));
const int64_t nrows_slab = std::max<int64_t>(1, std::min<int64_t>(nrows, max_buf_size/bytes_per_row));
static const int64_t min_chunk_size = 32 * 512;
const int64_t chunk_size = (n_per_row >= min_chunk_size ? n_per_row : n_per_row * ((min_chunk_size + n_per_row - 1)/n_per_row));
const int64_t nelements_matrix = tensor->ne[0] * tensor->ne[1];
const int64_t nchunk = (nelements_matrix + chunk_size - 1)/chunk_size;
const int64_t nthread_use = nthread > 1 ? std::max((int64_t)1, std::min((int64_t)nthread, nchunk)) : 1;
// quantize each expert separately since they have different importance matrices
new_size = 0;
for (int64_t i03 = 0; i03 < tensor->ne[2]; ++i03) {
const float * f32_data_03 = f32_data + i03 * nelements_matrix;
void * new_data_03 = (char *)new_data + ggml_row_size(new_type, n_per_row) * i03 * nrows;
const float * imatrix_03 = imatrix ? imatrix + i03 * n_per_row : nullptr;
new_size += llama_tensor_quantize_impl(new_type, f32_data_03, new_data_03, chunk_size, nrows, n_per_row, imatrix_03, workers, nthread_use);
for (int64_t ir = 0; ir < nrows; ir += nrows_slab) {
const int64_t nrows_cur = std::min(nrows_slab, nrows - ir);
const int64_t nelements_cur = nrows_cur * n_per_row;
const void * src = load_range((i03*nrows + ir)*row_size_src, nrows_cur*row_size_src);
const float * f32_data;
if (tensor->type == GGML_TYPE_F32) {
f32_data = (const float *) src;
} else {
if (f32_conv_buf.size() < (size_t) nelements_cur) {
f32_conv_buf.resize(nelements_cur);
}
llama_tensor_dequantize_impl(tensor->type, src, (float *) f32_conv_buf.data(), workers, nelements_cur, nthread);
f32_data = (const float *) f32_conv_buf.data();
}
if (work.size() < nrows_cur*row_size_dst) {
work.resize(nrows_cur*row_size_dst);
}
const int64_t nchunk = (nelements_cur + chunk_size - 1)/chunk_size;
const int64_t nthread_use = nthread > 1 ? std::max((int64_t)1, std::min((int64_t)nthread, nchunk)) : 1;
const size_t size_cur = llama_tensor_quantize_impl(new_type, f32_data, work.data(), chunk_size, nrows_cur, n_per_row, imatrix_03, workers, nthread_use);
fout.write((const char *) work.data(), size_cur);
new_size += size_cur;
}
}
LLAMA_LOG_INFO("size = %8.2f MiB -> %8.2f MiB\n", tensor_size/1024.0/1024.0, new_size/1024.0/1024.0);
}
@@ -1273,10 +1313,8 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std::
// update the gguf metadata as we go
gguf_set_tensor_type(ctx_outs[cur_split].get(), metadata[i].name.c_str(), new_type);
GGML_ASSERT(gguf_get_tensor_size(ctx_outs[cur_split].get(), gguf_find_tensor(ctx_outs[cur_split].get(), metadata[i].name.c_str())) == new_size);
gguf_set_tensor_data(ctx_outs[cur_split].get(), metadata[i].name.c_str(), new_data);
// write tensor data + padding
fout.write((const char *) new_data, new_size);
// tensor data is already written, add the padding
zeros(fout, GGML_PAD(new_size, align) - new_size);
// unmap the tensor to free memory
@@ -1323,7 +1361,8 @@ llama_model_quantize_params llama_model_quantize_default_params() {
/*.imatrix =*/ nullptr,
/*.kv_overrides =*/ nullptr,
/*.tensor_type =*/ nullptr,
/*.prune_layers =*/ nullptr
/*.prune_layers =*/ nullptr,
/*.max_buf_size =*/ LLAMA_QUANT_MAX_BUF_SIZE
};
return result;
+2
View File
@@ -318,6 +318,8 @@ static std::pair<int, llama_model *> llama_model_load(struct gguf_context * meta
llama_model_loader ml(metadata, set_tensor_data, set_tensor_data_ud, fname, splits, file, params.load_mode,
params.check_tensors, params.no_alloc, params.load_mtp, params.kv_overrides, params.tensor_buft_overrides);
ml.tensor_read_lazy = params.tensor_read_lazy;
ml.print_info();
std::unique_ptr<llama_model> model_ptr(llama_model_create(ml, params));
+299 -39
View File
@@ -7,6 +7,18 @@
void llama_model_dflash::load_arch_hparams(llama_model_loader & ml) {
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
ml.get_key(LLM_KV_LOGIT_SCALE, hparams.f_logit_scale, false);
hparams.f_final_logit_softcapping = 0.0f;
ml.get_key(LLM_KV_FINAL_LOGIT_SOFTCAPPING, hparams.f_final_logit_softcapping, false);
// drafts for M-RoPE targets carry degenerate sections [n_rot/2, 0, 0, 0]
ml.get_key_or_arr(LLM_KV_ROPE_DIMENSION_SECTIONS, hparams.rope_sections, 4, false);
ml.get_key(LLM_KV_DFLASH_BLOCK_SIZE, hparams.dflash_block_size, false);
ml.get_key(LLM_KV_DFLASH_CONV_KERNEL_SIZE, hparams.dflash_conv_kernel_size, false);
ml.get_key(LLM_KV_DFLASH_CONV_GROUP_SIZE, hparams.dflash_conv_group_size, false);
ml.get_key(LLM_KV_DFLASH_SELECTOR_RANK, hparams.dflash_selector_rank, false);
ml.get_key(LLM_KV_DFLASH_SELECTOR_TOP_K, hparams.dflash_selector_top_k, false);
if (!ml.get_arr(LLM_KV_TARGET_LAYERS, target_layer_ids, false)) {
throw std::runtime_error("DFlash model requires 'target_layers' in GGUF metadata");
@@ -103,15 +115,39 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) {
if (markov_meta) {
const int64_t dspark_markov_rank = markov_meta->ne[0];
dspark_markov_w1 = create_tensor(tn(LLM_TENSOR_DSPARK_MARKOV_W1, "weight"), { dspark_markov_rank, n_vocab }, 0);
dspark_markov_w2 = create_tensor(tn(LLM_TENSOR_DSPARK_MARKOV_W2, "weight"), { dspark_markov_rank, n_vocab_draft }, 0);
dspark_markov_w1 = create_tensor(tn(LLM_TENSOR_DSPARK_MARKOV_W1, "weight"), { dspark_markov_rank, n_vocab }, 0);
dspark_markov_w2 = create_tensor(tn(LLM_TENSOR_DSPARK_MARKOV_W2, "weight"), { dspark_markov_rank, n_vocab_draft }, 0);
dspark_markov_w2_s = create_tensor(tn(LLM_TENSOR_DSPARK_MARKOV_W2, "scale"), { 1 }, TENSOR_NOT_REQUIRED);
dspark_conf_proj = create_tensor(tn(LLM_TENSOR_DSPARK_CONF_PROJ, "weight"), { n_embd + dspark_markov_rank, 1 }, 0);
dspark_conf_proj = create_tensor(tn(LLM_TENSOR_DSPARK_CONF_PROJ, "weight"), { n_embd + dspark_markov_rank, 1 }, TENSOR_NOT_REQUIRED);
dspark_conf_proj_b = create_tensor(tn(LLM_TENSOR_DSPARK_CONF_PROJ, "bias"), { 1 }, TENSOR_NOT_REQUIRED);
LLAMA_LOG_INFO("%s: DFlash with DSpark markov head (rank = %lld)\n", __func__, (long long) dspark_markov_rank);
}
const struct ggml_tensor * selector_meta = ml->get_tensor_meta("selector_hidden.weight");
if (selector_meta) {
const int64_t rank = hparams.dflash_selector_rank;
if (rank <= 0 || hparams.dflash_block_size <= 0 || hparams.dflash_selector_top_k <= 0 ||
hparams.dflash_conv_kernel_size <= 0 || hparams.dflash_conv_group_size <= 0) {
throw std::runtime_error("DFlash2 model is missing conv/selector metadata");
}
if (n_embd % hparams.dflash_conv_group_size != 0) {
throw std::runtime_error("DFlash2 hidden size must be divisible by conv_group_size");
}
if (n_embd < hparams.dflash_selector_top_k * (hparams.dflash_selector_top_k + 1)) {
throw std::runtime_error("DFlash2 hidden size is too small for the selector lattice");
}
dflash_selector_prev = create_tensor(tn(LLM_TENSOR_DFLASH_SELECTOR_PREV, "weight"), { rank, n_vocab }, 0);
dflash_selector_next = create_tensor(tn(LLM_TENSOR_DFLASH_SELECTOR_NEXT, "weight"), { rank, n_vocab }, 0);
dflash_selector_hidden = create_tensor(tn(LLM_TENSOR_DFLASH_SELECTOR_HIDDEN, "weight"), { n_embd, rank }, 0);
LLAMA_LOG_INFO("%s: DFlash2 conv kernel = %u, group = %u, selector rank = %u, top-k = %u\n", __func__,
hparams.dflash_conv_kernel_size, hparams.dflash_conv_group_size,
hparams.dflash_selector_rank, hparams.dflash_selector_top_k);
}
fc = create_tensor(tn(LLM_TENSOR_FC, "weight"), { n_embd_inp, n_embd }, 0);
fc_s = create_tensor(tn(LLM_TENSOR_FC, "scale"), { 1 }, TENSOR_NOT_REQUIRED);
output_norm_enc = create_tensor(tn(LLM_TENSOR_ENC_OUTPUT_NORM, "weight"), { n_embd }, 0); // encoder hidden_norm (after fc)
@@ -184,10 +220,23 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) {
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), { n_embd_head_k }, 0);
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), { n_embd_head_k }, 0);
// optional per-head attention sinks (e.g. Nemotron DSpark)
layer.attn_sinks = create_tensor(tn(LLM_TENSOR_ATTN_SINKS, "weight", i), { n_head }, TENSOR_NOT_REQUIRED);
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), { n_embd }, 0);
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), { n_embd, n_ff }, 0);
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd }, 0);
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), { n_embd, n_ff }, 0);
if (selector_meta) {
const int64_t kernel = hparams.dflash_conv_kernel_size;
const int64_t groups = n_embd / hparams.dflash_conv_group_size;
const int64_t projected = 2 * kernel * groups;
layer.dflash_attn_conv_base = create_tensor(tn(LLM_TENSOR_DFLASH_ATTN_CONV_BASE, i), { n_embd, kernel, 2 }, 0);
layer.dflash_attn_conv_proj = create_tensor(tn(LLM_TENSOR_DFLASH_ATTN_CONV_PROJ, "weight", i), { n_embd, projected }, 0);
layer.dflash_ffn_conv_base = create_tensor(tn(LLM_TENSOR_DFLASH_FFN_CONV_BASE, i), { n_embd, kernel, 2 }, 0);
layer.dflash_ffn_conv_proj = create_tensor(tn(LLM_TENSOR_DFLASH_FFN_CONV_PROJ, "weight", i), { n_embd, projected }, 0);
}
}
}
@@ -245,7 +294,10 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
ggml_tensor * w1 = model.dspark_markov_w1;
ggml_tensor * w2 = model.dspark_markov_w2;
GGML_ASSERT(w1 && w2 && model.dspark_conf_proj && "DSpark markov/confidence weights not loaded");
GGML_ASSERT(w1 && w2 && "DSpark markov weights not loaded");
// confidence head is optional
const bool has_conf = model.dspark_conf_proj != nullptr;
ggml_tensor * base = res->t_logits; // [n_vocab, n_tokens]
const int64_t n_vocab = base->ne[0];
@@ -276,23 +328,22 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
ggml_tensor * prev = ggml_view_2d(ctx0, tokens, 1, n_blocks, token_stride, 0);
prev = ggml_cont_1d(ctx0, prev, n_blocks);
// confidence head input: predicts per-position acceptance
ggml_tensor * conf_inp = res->t_embd; // [n_embd, n_tok]
ggml_tensor * cat = nullptr;
ggml_tensor * cat_conf = nullptr;
if (!sample_from_anchor) {
// bonus anchor slot: pass the logits through unbiased, pad the (unread) confidence column
cat = ggml_cont(ctx0, ggml_view_2d(ctx0, base, n_vocab, n_blocks, base_stride, 0));
cat_conf = ggml_sigmoid(ctx0, ggml_cont(ctx0, ggml_view_2d(ctx0, base, 1, n_blocks, base_stride, 0)));
cat = ggml_cont(ctx0, ggml_view_2d(ctx0, base, n_vocab, n_blocks, base_stride, 0));
if (has_conf) {
cat_conf = ggml_sigmoid(ctx0, ggml_cont(ctx0, ggml_view_2d(ctx0, base, 1, n_blocks, base_stride, 0)));
}
}
// TODO: the in-graph chain is greedy (argmax); sampling params affect only the final
// token pick, not the Markov conditioning path
for (int64_t i = i_draft_beg; i < block_drafts; ++i) {
ggml_tensor * w1_prev = ggml_get_rows(ctx0, w1, prev); // [R, n_blocks]
ggml_tensor * bias = ggml_mul_mat(ctx0, w2, w1_prev); // [n_vocab_draft, n_blocks]
ggml_tensor * w1_prev = ggml_get_rows(ctx0, w1, prev); // [R, n_blocks]
ggml_tensor * bias = g.build_lora_mm(w2, w1_prev, model.dspark_markov_w2_s); // [n_vocab_draft, n_blocks]
if (model.d2t) {
// reduced draft vocab: scatter the bias to the target rows (base is -inf on the others)
const int64_t n_draft_vocab = bias->ne[0];
@@ -309,17 +360,21 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
cat = cat ? ggml_concat(ctx0, cat, col, 1) : col;
// conf(i) = sigmoid(conf_proj . [conf_inp(i); markov_w1[prev(i)]] + b) -- [1, n_blocks]
ggml_tensor * conf_inp_i = ggml_view_2d(ctx0, conf_inp, conf_inp->ne[0], n_blocks,
(size_t) block_drafts * conf_inp->nb[1], i*conf_inp->nb[1]);
ggml_tensor * feat = ggml_concat(ctx0, ggml_cont(ctx0, conf_inp_i), w1_prev, 0);
ggml_tensor * conf = ggml_mul_mat(ctx0, model.dspark_conf_proj, feat);
if (model.dspark_conf_proj_b) {
conf = ggml_add(ctx0, conf, model.dspark_conf_proj_b);
}
conf = ggml_sigmoid(ctx0, conf);
if (has_conf) {
// confidence head input: predicts per-position acceptance
ggml_tensor * conf_inp = res->t_embd; // [n_embd, n_tok]
// conf(i) = sigmoid(conf_proj . [conf_inp(i); markov_w1[prev(i)]] + b) -- [1, n_blocks]
ggml_tensor * conf_inp_i = ggml_view_2d(ctx0, conf_inp, conf_inp->ne[0], n_blocks,
(size_t) block_drafts * conf_inp->nb[1], i*conf_inp->nb[1]);
ggml_tensor * feat = ggml_concat(ctx0, ggml_cont(ctx0, conf_inp_i), w1_prev, 0);
ggml_tensor * conf = ggml_mul_mat(ctx0, model.dspark_conf_proj, feat);
if (model.dspark_conf_proj_b) {
conf = ggml_add(ctx0, conf, model.dspark_conf_proj_b);
}
conf = ggml_sigmoid(ctx0, conf);
cat_conf = cat_conf ? ggml_concat(ctx0, cat_conf, conf, 1) : conf;
cat_conf = cat_conf ? ggml_concat(ctx0, cat_conf, conf, 1) : conf;
}
if (i + 1 < block_drafts) {
prev = ggml_argmax(ctx0, col);
@@ -331,7 +386,7 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
out = ggml_cont(ctx0, ggml_permute(ctx0, out, 0, 2, 1, 3)); // [n_vocab, block_drafts, n_blocks]
out = ggml_reshape_2d(ctx0, out, n_vocab, n_tok);
{
if (has_conf) {
ggml_tensor * conf = ggml_reshape_3d(ctx0, cat_conf, 1, n_blocks, block_drafts);
conf = ggml_cont(ctx0, ggml_permute(ctx0, conf, 0, 2, 1, 3));
conf = ggml_reshape_2d(ctx0, conf, 1, n_tok);
@@ -346,6 +401,167 @@ static void build_dspark_markov_head(llm_graph_context & g, const llama_model &
ggml_build_forward_expand(g.gf, out);
}
static ggml_tensor * build_dflash2_conv(
llm_graph_context & g,
ggml_tensor * hidden,
ggml_tensor * dynamic,
ggml_tensor * base,
int side) {
const auto & hparams = g.hparams;
const int64_t hidden_size = hidden->ne[0];
const int64_t n_tokens = hidden->ne[1];
const int64_t n_blocks = g.ubatch.n_seqs_unq;
const int64_t kernel_size = hparams.dflash_conv_kernel_size;
const int64_t group_size = hparams.dflash_conv_group_size;
const int64_t n_groups = hidden_size / group_size;
GGML_ASSERT(n_blocks > 0 && n_tokens % n_blocks == 0);
GGML_ASSERT(dynamic && base && side >= 0 && side < 2);
const int64_t block_size = n_tokens / n_blocks;
ggml_context * ctx0 = g.ctx0;
// ggml_cont copies even when the tensor is already contiguous
if (!ggml_is_contiguous(hidden) || hidden->ne[1] != n_tokens) {
hidden = ggml_cont_2d(ctx0, hidden, hidden_size, n_tokens);
}
if (!ggml_is_contiguous(dynamic) || dynamic->ne[1] != n_tokens) {
dynamic = ggml_cont_2d(ctx0, dynamic, dynamic->ne[0], n_tokens);
}
ggml_tensor * blocks = ggml_reshape_3d(ctx0, hidden, hidden_size, block_size, n_blocks);
ggml_tensor * coeffs = ggml_reshape_4d(ctx0, dynamic, n_groups, kernel_size, 2, n_tokens);
ggml_tensor * coeffs_side = ggml_view_3d(ctx0, coeffs, n_groups, kernel_size, n_tokens,
coeffs->nb[1], coeffs->nb[3], side * coeffs->nb[2]);
ggml_tensor * coeff_all = ggml_cont(ctx0, coeffs_side);
coeff_all = ggml_reshape_4d(ctx0, coeff_all, 1, n_groups, kernel_size, n_tokens);
coeff_all = ggml_repeat_4d(ctx0, coeff_all, group_size, n_groups, kernel_size, n_tokens);
ggml_tensor * base_side = ggml_reshape_4d(ctx0,
ggml_view_1d(ctx0, base, hidden_size * kernel_size, side * base->nb[2]),
group_size, n_groups, kernel_size, 1);
ggml_tensor * weight_all = ggml_add(ctx0, coeff_all, base_side);
ggml_tensor * result = nullptr;
for (int64_t tap = 0; tap < kernel_size; ++tap) {
ggml_tensor * values = blocks;
if (tap > 0) {
ggml_tensor * zeros = ggml_fill(ctx0,
ggml_new_tensor_3d(ctx0, hidden->type, hidden_size, std::min(tap, block_size), n_blocks), 0.0f);
if (tap < block_size) {
ggml_tensor * previous = ggml_view_3d(ctx0, blocks, hidden_size, block_size - tap, n_blocks,
blocks->nb[1], blocks->nb[2], 0);
values = ggml_concat(ctx0, zeros, previous, 1);
} else {
values = zeros;
}
}
values = ggml_reshape_2d(ctx0, values, hidden_size, n_tokens);
ggml_tensor * weight = ggml_reshape_2d(ctx0,
ggml_cont(ctx0, ggml_view_4d(ctx0, weight_all, group_size, n_groups, 1, n_tokens,
weight_all->nb[1], weight_all->nb[2], weight_all->nb[3], tap * weight_all->nb[2])),
hidden_size, n_tokens);
ggml_tensor * term = ggml_mul(ctx0, weight, values);
result = result ? ggml_add(ctx0, result, term) : term;
}
return result;
}
// DFlash2 selector: top-k candidates per block position plus the pairwise
// transition scores, packed into the nextn output slot for the CPU-side walk.
static void build_dflash2_selector(llm_graph_context & g, const llama_model & model, ggml_tensor * tokens) {
ggml_context * ctx0 = g.ctx0;
auto & res = g.res;
const auto & hparams = g.hparams;
const int64_t n_tokens = g.n_tokens;
const int64_t n_embd = g.n_embd;
const int64_t top_k = hparams.dflash_selector_top_k;
const int64_t rank = hparams.dflash_selector_rank;
const int64_t n_blocks = g.ubatch.n_seqs_unq;
GGML_ASSERT(n_blocks > 0 && n_tokens % n_blocks == 0);
GGML_ASSERT(res->t_logits->ne[1] == n_tokens);
if (!tokens) {
return;
}
const int64_t tokens_per_block = n_tokens / n_blocks;
const int64_t block_size = std::min<int64_t>(tokens_per_block, hparams.dflash_block_size);
const int64_t row_used = top_k + top_k * top_k;
ggml_tensor * candidates = ggml_top_k(ctx0, res->t_logits, top_k);
ggml_tensor * logits_rows = ggml_reshape_3d(ctx0, res->t_logits, 1, res->t_logits->ne[0], n_tokens);
ggml_tensor * unary = ggml_reshape_2d(ctx0,
ggml_get_rows(ctx0, logits_rows, candidates), top_k, n_tokens);
ggml_tensor * gate = g.build_lora_mm(model.dflash_selector_hidden, res->t_embd);
// Everything below indexes [.., tokens_per_block, n_blocks]: the block
// position varies fastest, sequences are the outer dimension.
ggml_tensor * cand_blk = ggml_reshape_3d(ctx0, candidates, top_k, tokens_per_block, n_blocks);
ggml_tensor * unary_blk = ggml_reshape_3d(ctx0, unary, top_k, tokens_per_block, n_blocks);
ggml_tensor * gate_blk = ggml_reshape_3d(ctx0, gate, rank, tokens_per_block, n_blocks);
// a position's score reads only the candidate sets at pos-1 and pos, so a run
// of positions has no internal dependency and scores in one batched matmul
auto score_run = [&](int64_t beg_pos, int64_t n_pos, ggml_tensor * pred_ids) {
ggml_tensor * cand_run = ggml_cont(ctx0, ggml_view_3d(ctx0, cand_blk, top_k, n_pos, n_blocks,
cand_blk->nb[1], cand_blk->nb[2], beg_pos * cand_blk->nb[1]));
ggml_tensor * unary_run = ggml_cont(ctx0, ggml_view_3d(ctx0, unary_blk, top_k, n_pos, n_blocks,
unary_blk->nb[1], unary_blk->nb[2], beg_pos * unary_blk->nb[1]));
ggml_tensor * gate_run = ggml_cont(ctx0, ggml_view_3d(ctx0, gate_blk, rank, n_pos, n_blocks,
gate_blk->nb[1], gate_blk->nb[2], beg_pos * gate_blk->nb[1]));
const int64_t n_pred = pred_ids->ne[0] / (n_pos * n_blocks);
ggml_tensor * successor = ggml_reshape_4d(ctx0,
ggml_get_rows(ctx0, model.dflash_selector_next, ggml_reshape_1d(ctx0, cand_run, top_k * n_pos * n_blocks)),
rank, top_k, n_pos, n_blocks);
ggml_tensor * predecessor = ggml_reshape_4d(ctx0,
ggml_get_rows(ctx0, model.dflash_selector_prev, pred_ids),
rank, n_pred, n_pos, n_blocks);
ggml_tensor * gate_bcast = ggml_reshape_4d(ctx0, gate_run, rank, 1, n_pos, n_blocks);
ggml_tensor * cond = ggml_mul(ctx0, predecessor, ggml_repeat(ctx0, gate_bcast, predecessor));
ggml_tensor * score = ggml_mul_mat(ctx0, successor, cond);
if (n_pred == 1) {
score = ggml_repeat_4d(ctx0, score, top_k, top_k, n_pos, n_blocks);
}
ggml_tensor * unary_bcast = ggml_reshape_4d(ctx0, unary_run, top_k, 1, n_pos, n_blocks);
score = ggml_add(ctx0, score, ggml_repeat(ctx0, unary_bcast, score));
ggml_tensor * row = ggml_concat(ctx0,
ggml_cast(ctx0, cand_run, GGML_TYPE_F32),
ggml_reshape_3d(ctx0, score, top_k * top_k, n_pos, n_blocks), 0);
return ggml_pad(ctx0, row, n_embd - row_used, 0, 0, 0);
};
ggml_tensor * packed = ggml_fill(ctx0,
ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, n_embd, 1, n_blocks), 0.0f);
if (block_size > 1) {
// Position 1 alone: its predecessor is the anchor token, one id per
// sequence rather than a candidate set.
ggml_tensor * anchor_ids = ggml_cont_1d(ctx0,
ggml_view_2d(ctx0, tokens, 1, n_blocks, tokens_per_block * tokens->nb[0], 0), n_blocks);
packed = ggml_concat(ctx0, packed, score_run(1, 1, anchor_ids), 1);
}
if (block_size > 2) {
ggml_tensor * prev_ids = ggml_reshape_1d(ctx0,
ggml_cont(ctx0, ggml_view_3d(ctx0, cand_blk, top_k, block_size - 2, n_blocks,
cand_blk->nb[1], cand_blk->nb[2], cand_blk->nb[1])),
top_k * (block_size - 2) * n_blocks);
packed = ggml_concat(ctx0, packed, score_run(2, block_size - 2, prev_ids), 1);
}
packed = ggml_reshape_2d(ctx0, packed, n_embd, block_size * n_blocks);
g.cb(packed, "dflash2_lattice", -1);
res->t_h_nextn = packed;
ggml_build_forward_expand(g.gf, packed);
}
// DFlash decoder, dual-mode by batch type:
// * embd batch -> fused target features: project + inject K/V into the cache.
// * token batch -> noise-block diffusion: attend over [committed, MASK...] to generate draft tokens
@@ -370,6 +586,20 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
const float kq_scale = 1.0f/sqrtf(float(n_embd_head));
// drafts for M-RoPE targets use degenerate sections (temporal dim only)
int sections[4];
std::copy(std::begin(hparams.rope_sections), std::begin(hparams.rope_sections) + 4, sections);
auto build_rope = [&](ggml_tensor * cur, ggml_tensor * pos) {
return rope_type == GGML_ROPE_TYPE_MROPE
? ggml_rope_multi(ctx0, cur, pos, nullptr,
n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow)
: ggml_rope_ext(ctx0, cur, pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow);
};
// KV cache injection
if (ubatch.embd) {
auto inp = std::make_unique<llm_graph_input_embd>(n_embd);
@@ -392,11 +622,7 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);
Kcur = ggml_rope_ext(
ctx0, Kcur, inp_pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Kcur = build_rope(Kcur, inp_pos);
cb(Kcur, "Kcur_injected", il);
cb(Vcur, "Vcur_injected", il);
@@ -450,6 +676,7 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
inp->tokens = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_tokens);
ggml_set_input(inp->tokens);
res->t_inp_tokens = inp->tokens;
ggml_tensor * inp_tokens = inp->tokens;
@@ -464,6 +691,13 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
ggml_tensor * noise_norm = build_norm(inpL, layer.attn_norm, NULL, LLM_NORM_RMS, il);
cb(noise_norm, "noise_norm", il);
ggml_tensor * attn_dynamic = nullptr;
if (layer.dflash_attn_conv_proj) {
attn_dynamic = build_lora_mm(layer.dflash_attn_conv_proj, noise_norm);
noise_norm = build_dflash2_conv(*this, noise_norm, attn_dynamic, layer.dflash_attn_conv_base, 0);
cb(noise_norm, "attn_conv_in", il);
}
ggml_tensor * Qcur = build_lora_mm(layer.wq, noise_norm);
ggml_tensor * Kcur = build_lora_mm(layer.wk, noise_norm);
ggml_tensor * Vcur = build_lora_mm(layer.wv, noise_norm);
@@ -475,24 +709,21 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
Qcur = build_norm(Qcur, layer.attn_q_norm, NULL, LLM_NORM_RMS, il);
Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);
Qcur = ggml_rope_ext(
ctx0, Qcur, inp_pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Kcur = ggml_rope_ext(
ctx0, Kcur, inp_pos, nullptr,
n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
ext_factor, attn_factor, beta_fast, beta_slow
);
Qcur = build_rope(Qcur, inp_pos);
Kcur = build_rope(Kcur, inp_pos);
cb(Qcur, "Qcur", il);
cb(Kcur, "Kcur", il);
cb(Vcur, "Vcur", il);
// cache-aware, non-causal attention
ggml_tensor * cur = use_iswa
? build_attn(inp_attn_iswa, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il)
: build_attn(inp_attn, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
? build_attn(inp_attn_iswa, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, layer.attn_sinks, nullptr, kq_scale, il)
: build_attn(inp_attn, layer.wo, NULL, NULL, Qcur, Kcur, Vcur, nullptr, layer.attn_sinks, nullptr, kq_scale, il);
if (attn_dynamic) {
cur = build_dflash2_conv(*this, cur, attn_dynamic, layer.dflash_attn_conv_base, 1);
cb(cur, "attn_conv_out", il);
}
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpL);
cb(ffn_inp, "ffn_inp", il);
@@ -500,6 +731,13 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
cur = build_norm(ffn_inp, layer.ffn_norm, NULL, LLM_NORM_RMS, il);
cb(cur, "ffn_norm", il);
ggml_tensor * ffn_dynamic = nullptr;
if (layer.dflash_ffn_conv_proj) {
ffn_dynamic = build_lora_mm(layer.dflash_ffn_conv_proj, cur);
cur = build_dflash2_conv(*this, cur, ffn_dynamic, layer.dflash_ffn_conv_base, 0);
cb(cur, "ffn_conv_in", il);
}
cur = build_ffn(cur,
layer.ffn_up, NULL, layer.ffn_up_s,
layer.ffn_gate, NULL, layer.ffn_gate_s,
@@ -508,6 +746,11 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
LLM_FFN_SILU, LLM_FFN_PAR, il);
cb(cur, "ffn_out", il);
if (ffn_dynamic) {
cur = build_dflash2_conv(*this, cur, ffn_dynamic, layer.dflash_ffn_conv_base, 1);
cb(cur, "ffn_conv_out", il);
}
cur = ggml_add(ctx0, cur, ffn_inp);
cb(cur, "l_out", il);
@@ -532,6 +775,19 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
cur = build_lora_mm(output, cur, output_s);
// DFlash2 feeds these logits to the selector, so they need the target's output
// transforms; DFlash1 and DSpark read them through the sampler instead
if (model.dflash_selector_hidden) {
if (hparams.f_logit_scale != 0.0f) {
cur = ggml_scale(ctx0, cur, hparams.f_logit_scale);
}
if (hparams.f_final_logit_softcapping > 0.0f) {
cur = ggml_scale(ctx0, cur, 1.0f / hparams.f_final_logit_softcapping);
cur = ggml_tanh(ctx0, cur);
cur = ggml_scale(ctx0, cur, hparams.f_final_logit_softcapping);
}
}
// reduced-draft-vocab exports: scatter the draft logits to the target vocabulary via d2t
if (model.d2t) {
const int64_t n_draft_vocab = cur->ne[0];
@@ -556,6 +812,10 @@ llama_model_dflash::graph<false>::graph(const llama_model & model, const llm_gra
if (model.dspark_markov_w1) {
build_dspark_markov_head(*this, model, inp_tokens);
}
if (model.dflash_selector_hidden) {
build_dflash2_selector(*this, model, inp_tokens);
}
}
// DSV4 DSpark decoder, dual-mode by batch type (see the DFlash decoder above):
+1 -1
View File
@@ -50,7 +50,7 @@ void llama_model_gemma4::load_arch_tensors(llama_model_loader &) {
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
if (n_embd_per_layer > 0) {
per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), {n_embd_per_layer * n_layer, n_vocab}, 0);
per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"), {n_embd_per_layer * n_layer, n_vocab}, TENSOR_READ_LAZY);
per_layer_model_proj = create_tensor(tn(LLM_TENSOR_PER_LAYER_MODEL_PROJ, "weight", 0), {n_embd, n_embd_per_layer * n_layer}, 0);
per_layer_proj_norm = create_tensor(tn(LLM_TENSOR_PER_LAYER_PROJ_NORM, "weight", 0), {n_embd_per_layer}, 0);
}
+15 -52
View File
@@ -174,16 +174,14 @@ public:
bool can_reuse(const llm_graph_params & params) override {
bool res = true;
if (params.ubatch.n_seq_tokens > 1) {
res &= ( inp_q_decay && inp_q_decay->ne[2] == params.ubatch.n_seq_tokens);
res &= ( inp_k_decay && inp_k_decay->ne[2] == params.ubatch.n_seq_tokens);
res &= (inp_diag_decay && inp_diag_decay->ne[1] == params.ubatch.n_seq_tokens);
}
res &= ( inp_q_decay && inp_q_decay->ne[2] == params.ubatch.n_seq_tokens);
res &= ( inp_k_decay && inp_k_decay->ne[2] == params.ubatch.n_seq_tokens);
res &= (inp_diag_decay && inp_diag_decay->ne[1] == params.ubatch.n_seq_tokens);
return res;
}
const llama_hparams & hparams;
const llama_hparams hparams;
ggml_tensor * inp_slopes = nullptr; // F32 [n_head]
ggml_tensor * inp_q_decay = nullptr; // F32 [1, n_head, n_batch]
@@ -223,19 +221,17 @@ llama_model_minimax_01::graph::graph(const llama_model & model, const llm_graph_
ggml_set_input(inp->inp_slopes);
cb(inp->inp_slopes, "slopes", -1);
if (n_seq_tokens != 1) {
inp->inp_q_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, 1, n_head, n_seq_tokens, n_seqs);
ggml_set_input(inp->inp_q_decay);
cb(inp->inp_q_decay, "q_decay_exp", -1);
inp->inp_q_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, 1, n_head, n_seq_tokens, n_seqs);
ggml_set_input(inp->inp_q_decay);
cb(inp->inp_q_decay, "q_decay_exp", -1);
inp->inp_k_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, 1, n_head, n_seq_tokens, n_seqs);
ggml_set_input(inp->inp_k_decay);
cb(inp->inp_k_decay, "k_decay_exp", -1);
inp->inp_k_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, 1, n_head, n_seq_tokens, n_seqs);
ggml_set_input(inp->inp_k_decay);
cb(inp->inp_k_decay, "k_decay_exp", -1);
inp->inp_diag_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, n_seq_tokens, n_seq_tokens, n_head, n_seqs);
ggml_set_input(inp->inp_diag_decay);
cb(inp->inp_diag_decay, "diag_decay_exp", -1);
}
inp->inp_diag_decay = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, n_seq_tokens, n_seq_tokens, n_head, n_seqs);
ggml_set_input(inp->inp_diag_decay);
cb(inp->inp_diag_decay, "diag_decay_exp", -1);
la = (llm_graph_input_la *) res->add_input(std::move(inp));
@@ -319,41 +315,8 @@ llama_model_minimax_01::graph::graph(const llama_model & model, const llm_graph_
ggml_tensor * qkv = nullptr;
ggml_tensor * kv_new = nullptr;
if (n_seq_tokens == 1) {
// lightning attention - optimized single token case for TG
ggml_tensor * slopes_neg = ggml_scale(ctx0, slope_rate, -1.0);
cb(slopes_neg, "slopes_neg", il);
ggml_tensor * ratio = ggml_exp(ctx0, slopes_neg);
cb(ratio, "ratio", il);
ggml_tensor * ratio_3d = ggml_reshape_3d(ctx0, ratio, 1, 1, n_head);
cb(ratio_3d, "ratio3d", il);
ggml_tensor * v_trans = ggml_cont(ctx0, ggml_permute(ctx0, Vcur, 1, 2, 0, 3));
cb(v_trans, "v_trans", il);
ggml_tensor * k_trans = ggml_cont(ctx0, ggml_permute(ctx0, Kcur, 1, 2, 0, 3));
cb(k_trans, "k_trans", il);
ggml_tensor * kv_cur = ggml_mul_mat(ctx0, k_trans, v_trans);
cb(kv_cur, "kv_cur", il);
ggml_tensor * kv_old_s = ggml_mul(ctx0, kv_old, ratio_3d);
cb(kv_old_s, "kv_old_s", il);
kv_new = ggml_add(ctx0, kv_old_s, kv_cur);
cb(kv_new, "kv_new", il);
ggml_tensor * q_trans = ggml_permute(ctx0, Qcur, 0, 2, 1, 3);
cb(q_trans, "q_trans", il);
qkv = ggml_mul_mat(ctx0, kv_new, q_trans);
cb(qkv, "qkv", il);
} else if(n_seq_tokens > 1) {
// lightning attention - general multi token case for PP
{
// lightning attention
ggml_tensor * q_decay_exp = la->inp_q_decay;
ggml_tensor * k_decay_exp = la->inp_k_decay;
+105
View File
@@ -6,6 +6,9 @@
// note: almost all graphs require at least sqrtf, so include cmath globally
#include <cmath>
#include <map>
class llama_memory_hybrid_idx_context;
//
// base classes
@@ -2272,6 +2275,108 @@ struct llama_model_qwen35 : public llama_model_base {
};
struct llama_model_qwen4exp : public llama_model_base {
llama_model_qwen4exp(const struct llama_model_params & params) : llama_model_base(params) {}
class llm_graph_input_qsa;
void load_arch_hparams(llama_model_loader & ml) override;
void load_arch_tensors(llama_model_loader & ml) override;
struct graph : public llm_build_delta_net_base {
graph(const llama_model & model, const llm_graph_params & params);
private:
// HC replaces every layer norm: residual is [n_embd, hc, n_tokens]
ggml_tensor * build_hc_mix(
ggml_tensor * x,
ggml_tensor * w_norm,
ggml_tensor * w_down,
ggml_tensor * w_up,
ggml_tensor * w_inject,
ggml_tensor ** inject,
int il);
ggml_tensor * build_hc_combine(
ggml_tensor * residual,
ggml_tensor * block_out,
ggml_tensor * inject,
int il);
ggml_tensor * build_layer_attn(
llm_graph_input_attn_kv * inp_attn,
const llama_memory_hybrid_idx_context * mctx_hyb,
ggml_tensor * cur,
ggml_tensor * inp_pos,
int * sections,
int il);
// dense self-attention restricted to the cells that top_k names
ggml_tensor * build_attn_qsa(
llm_graph_input_attn_kv * inp,
ggml_tensor * q_cur,
ggml_tensor * k_cur,
ggml_tensor * v_cur,
ggml_tensor * top_k,
float kq_scale,
int il);
// the QSA cache layout inputs do not depend on the layer, only on its compress ratio,
// so the layers sharing a ratio share one input set
std::map<uint32_t, llm_graph_input_qsa *> qsa_inps;
// QSA: token indices this layer's queries may attend to, or nullptr for dense
ggml_tensor * build_qsa_top_k(
const llama_memory_hybrid_idx_context * mctx_hyb,
ggml_tensor * cur,
ggml_tensor * inp_pos,
ggml_tensor * kq_mask,
int * sections,
int il);
ggml_tensor * build_layer_attn_linear(
llm_graph_input_rs * inp,
ggml_tensor * cur,
int il);
ggml_tensor * build_layer_ffn(
ggml_tensor * cur,
int il);
ggml_tensor * build_norm_gated(
ggml_tensor * input,
ggml_tensor * weights,
ggml_tensor * gate,
int layer);
// build_rs writes the state tensor in place, so one gather per cache tensor is reused
std::map<ggml_tensor *, ggml_tensor *> rs_rows;
// one conv history per cache tensor: delta-net and PLE each have their own
ggml_tensor * build_conv_state_at(
llm_graph_input_rs * inp,
ggml_tensor * conv_states_all,
ggml_tensor * x,
int64_t state_cols,
int64_t channels,
int il);
ggml_tensor * build_ple(
llm_graph_input_rs * inp,
const llama_memory_hybrid_idx_context * mctx_hyb,
ggml_tensor * hidden,
int il);
// returns pair of qkv, z
std::pair<ggml_tensor *, ggml_tensor *> build_qkvz(
ggml_tensor * input,
int il);
const llama_model & model;
};
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
};
struct llama_model_qwen35moe : public llama_model_base {
llama_model_qwen35moe(const struct llama_model_params & params) : llama_model_base(params) {}
void load_arch_hparams(llama_model_loader & ml) override;
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -149,6 +149,7 @@ if (LLAMA_LLGUIDANCE)
endif ()
llama_build(test-recurrent-state-rollback.cpp)
llama_build(test-save-load-state.cpp)
if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
# these tests are disabled on Windows because they use internal functions not exported with LLAMA_API (when building with shared libraries)
@@ -237,6 +238,14 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
set_tests_properties(test-recurrent-state-rollback-dsv4 PROPERTIES
FIXTURES_REQUIRED generate-models
)
# Test state save/load functionality across all architectures, using the generated dummy models
llama_test(
test-save-load-state
LABEL main
ARGS --models "${MODEL_DIR}"
)
set_tests_properties(test-save-load-state PROPERTIES FIXTURES_REQUIRED generate-models)
endif()
llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp)
@@ -299,10 +308,6 @@ llama_build_and_test(test-backend-sampler.cpp LABEL "model")
llama_build_and_test(test-state-restore-fragmented.cpp LABEL "model" ARGS -m "${MODEL_DEST}")
set_tests_properties(test-state-restore-fragmented PROPERTIES FIXTURES_REQUIRED test-download-model)
# Test state save/load functionality
llama_build_and_test(test-save-load-state.cpp LABEL "model" ARGS -m "${MODEL_DEST}")
set_tests_properties(test-save-load-state PROPERTIES FIXTURES_REQUIRED test-download-model)
if (APPLE)
llama_build(test-rset-release.cpp)
endif()
+77
View File
@@ -4,6 +4,7 @@
#include "llama.h"
#include "speculative.h"
#include <cmath>
#include <limits>
#include <string>
#include <vector>
@@ -34,6 +35,62 @@ static void test(void) {
std::numeric_limits<int32_t>::max(),
std::numeric_limits<int32_t>::max());
{
common_params_speculative spec;
spec.synth_len = 3.4;
auto assert_invalid = [](const common_params_speculative & value, int32_t n_max) {
try {
common_speculative_synth_rates_resolve(&value, n_max);
assert(false);
} catch (const std::invalid_argument &) {
}
};
const auto rates = common_speculative_synth_rates_resolve(&spec, 4);
assert(rates.size() == 4);
assert(std::abs(rates[0] - 0.80581) < 1e-5);
assert(std::abs(rates[1] - 0.64933) < 1e-5);
assert(std::abs(rates[2] - 0.52323) < 1e-5);
assert(std::abs(rates[3] - 0.42163) < 1e-5);
assert(std::abs(1.0 + rates[0] + rates[1] + rates[2] + rates[3] - 3.4) < 1e-8);
spec.synth_len = 1.0;
assert(common_speculative_synth_rates_resolve(&spec, 4) == std::vector<double>({0.0, 0.0, 0.0, 0.0}));
spec.synth_len = 5.0;
assert(common_speculative_synth_rates_resolve(&spec, 4) == std::vector<double>({1.0, 1.0, 1.0, 1.0}));
spec.synth_len = 5.1;
assert_invalid(spec, 4);
spec.synth_len = std::numeric_limits<double>::quiet_NaN();
assert_invalid(spec, 4);
spec.synth_len = 0.0;
assert_invalid(spec, 4);
spec.synth_len = -1.0;
spec.synth_rates = {0.8, 0.6, 0.4};
assert_invalid(spec, 4);
spec.synth_rates = {0.8, 0.6, 0.4, 0.2};
assert(common_speculative_synth_rates_resolve(&spec, 4) == spec.synth_rates);
spec.synth_rates = {0.8, 0.9, 0.4, 0.2};
assert_invalid(spec, 4);
spec.synth_rates = {0.8, std::numeric_limits<double>::quiet_NaN(), 0.4, 0.2};
assert_invalid(spec, 4);
spec.synth_rates = {0.8, 0.6, 0.4, -0.2};
assert_invalid(spec, 4);
spec.synth_rates = {0.8, 0.6, 0.4, 0.2};
spec.synth_len = 3.0;
assert_invalid(spec, 4);
}
{
common_params base;
base.n_parallel = 4;
@@ -197,6 +254,26 @@ static void test(void) {
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_SPECULATIVE));
assert(params.speculative.draft.n_max == 123);
{
common_params synth_params;
argv = {"binary_name", "--spec-synth-len", "3.4"};
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), synth_params, LLAMA_EXAMPLE_SERVER));
assert(synth_params.speculative.synth_len == 3.4);
}
{
common_params synth_params;
argv = {"binary_name", "--spec-synth-rates", "0.8,0.6,0.2"};
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), synth_params, LLAMA_EXAMPLE_SERVER));
assert(synth_params.speculative.synth_rates == std::vector<double>({0.8, 0.6, 0.2}));
}
{
common_params synth_params;
argv = {"binary_name", "--spec-synth-len", "3.4x"};
assert(false == common_params_parse(argv.size(), list_str_to_char(argv).data(), synth_params, LLAMA_EXAMPLE_SERVER));
}
argv = {"binary_name", "-lm", "none"};
assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON));
assert(params.load_mode == LLAMA_LOAD_MODE_NONE);
+18 -1
View File
@@ -9717,6 +9717,17 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
}
}
}
for (int k : {4, 8, 16, 32}) {
for (int nrows : {1, 8, 16}) {
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {202048, nrows, 1, 1}, k));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {151936, nrows, 1, 1}, k));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {8192, nrows, 1, 1}, k));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {8193, nrows, 1, 1}, k));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {8192, nrows, 1, 1}, k, true));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {202048, nrows, 1, 1}, k, true));
}
}
for (int k : {1, 2, 3, 7, 15}) {
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {16, 10, 10, 10}, k));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {60, 10, 10, 10}, k));
@@ -10454,7 +10465,13 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_perf() {
test_cases.emplace_back(new test_argsort(GGML_TYPE_F32, {200000, 16, 1, 1}));
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {2, 1, 1, 1}, 1));
for (auto k : {1, 10, 40, 400}) {
// widths around the tiling threshold
for (auto cols : {4096, 8192, 12288, 16384, 24576, 32768, 65536, 131072}) {
for (auto nrows : {1, 16}) {
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, nrows, 1, 1}, 16));
}
}
for (auto k : {1, 4, 8, 10, 16, 32, 40, 400}) {
for (auto nrows : {1, 16}) {
for (auto cols : {k, 1000, 65000, 200000}) {
test_cases.emplace_back(new test_top_k(GGML_TYPE_F32, {cols, nrows, 1, 1}, k));
+22 -6
View File
@@ -65,7 +65,7 @@ static void set_tensor_data(struct ggml_tensor * tensor, void * userdata) {
}
static void usage(char ** argv) {
printf("Usage: %s [-a/--arch arch] [-s/--seed seed] [-v/--verbose]\n", argv[0]);
printf("Usage: %s [-a/--arch arch] [-s/--seed seed] [-o/--out dir] [-v/--verbose] [-h/--help]\n", argv[0]);
}
static std::vector<llama_token> get_tokens(const uint32_t n_tokens, const uint32_t n_vocab, const size_t seed){
@@ -82,7 +82,7 @@ static std::vector<llama_token> get_tokens(const uint32_t n_tokens, const uint32
static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
gguf_context_ptr ret(gguf_init_empty());
llama_model_saver ms(arch, ret.get());
const uint32_t n_ctx = 128;
const uint32_t n_ctx = 256;
uint32_t n_vocab = 128;
uint32_t n_embd = 256;
@@ -249,8 +249,19 @@ 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 || arch == LLM_ARCH_DEEPSEEK4 ? n_head : uint32_t(1));
ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, uint32_t(64));
if (arch == LLM_ARCH_QWEN4EXP) {
ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4));
ms.add_kv(LLM_KV_HYPER_CONNECTION_LOW_RANK, uint32_t(8));
// without this the QSA layers fall back to dense and go uncovered
ms.add_kv(LLM_KV_ATTENTION_COMPRESS_RATIOS, std::vector<uint32_t>(n_layer, 4));
}
// minimax-m3 keeps one indexer head per GQA head; the rest use a fixed 64 to match the fused
ms.add_kv(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, arch == LLM_ARCH_MINIMAX_M3 ? n_head : uint32_t(64));
// qwen4exp ropes indexer keys with the main rotary width, so its head can't be < n_rot
ms.add_kv(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH,
arch == LLM_ARCH_QWEN4EXP ? n_embd_head : uint32_t(128));
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));
@@ -294,7 +305,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_XIELU_ALPHA_P, 1.0f);
ms.add_kv(LLM_KV_XIELU_BETA, 1.0f);
ms.add_kv(LLM_KV_XIELU_EPS, 1.0e-7f);
ms.add_kv(LLM_KV_SSM_INNER_SIZE, arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE ? 256 : 2*n_embd);
ms.add_kv(LLM_KV_SSM_INNER_SIZE, arch == LLM_ARCH_QWEN3NEXT || arch == LLM_ARCH_QWEN35 || arch == LLM_ARCH_QWEN35MOE || arch == LLM_ARCH_QWEN4EXP ? 256 : 2*n_embd);
ms.add_kv(LLM_KV_SSM_CONV_KERNEL, uint32_t(4));
ms.add_kv(LLM_KV_SSM_STATE_SIZE, uint32_t(128));
ms.add_kv(LLM_KV_SSM_TIME_STEP_RANK, n_head);
@@ -411,6 +422,7 @@ static bool moe_mandatory(const llm_arch arch) {
case LLM_ARCH_QWEN3NEXT:
case LLM_ARCH_QWEN3VLMOE:
case LLM_ARCH_QWEN35MOE:
case LLM_ARCH_QWEN4EXP:
case LLM_ARCH_PHIMOE:
case LLM_ARCH_DBRX:
case LLM_ARCH_OLMOE:
@@ -507,7 +519,7 @@ static bool arch_supported(const llm_arch arch) {
}
// 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 || arch == LLM_ARCH_DOTS3NOTE) {
if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA || arch == LLM_ARCH_DOTS3NOTE || arch == LLM_ARCH_QWEN4EXP) {
return false;
}
#endif // GGML_USE_WEBGPU
@@ -752,6 +764,10 @@ int main(int argc, char ** argv) {
std::string out;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
usage(argv);
return 0;
}
if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--arch") == 0) {
if (i + 1 < argc) {
const std::string arch_name = argv[++i];
+120 -35
View File
@@ -3,8 +3,12 @@
#include "log.h"
#include "llama-cpp.h"
#include <algorithm>
#include <clocale>
#include <cstring>
#include <filesystem>
#include <random>
#include <string>
#include <vector>
struct llama_batch_ptr {
@@ -53,7 +57,9 @@ static llama_tokens generate_tokens(llama_context * ctx, llama_sampler * smpl, i
// - decode the last token
// - generate n_predict tokens
static llama_tokens test_baseline(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens) {
auto ctx = llama_context_ptr{llama_init_from_model(model, common_context_params_to_llama(params))};
auto params_ctx = common_context_params_to_llama(params);
params_ctx.n_seq_max = 2;
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
auto sparams = llama_sampler_chain_default_params();
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
@@ -161,7 +167,9 @@ static bool test_seq_rm_isolated(
// - replay the last prompt token
// - generate n_predict tokens and compare against expected result
static bool test_state_load(struct llama_model * model, const struct common_params & params, const llama_tokens & tokens, const llama_tokens & expected_result) {
auto ctx = llama_context_ptr{llama_init_from_model(model, common_context_params_to_llama(params))};
auto params_ctx = common_context_params_to_llama(params);
params_ctx.n_seq_max = 2;
auto ctx = llama_context_ptr{llama_init_from_model(model, params_ctx)};
auto sparams = llama_sampler_chain_default_params();
auto smpl = llama_sampler_ptr{llama_sampler_chain_init(sparams)};
@@ -347,38 +355,18 @@ static bool test_seq_cp_device(struct llama_model * model, const struct common_p
}
int main(int argc, char ** argv) {
std::setlocale(LC_NUMERIC, "C");
common_params params;
params.prompt = "";
params.n_batch = 100;
params.out_file = "dump_state.bin";
params.sampling.seed = 1234;
common_init();
if (!common_params_parse(argc, argv, params, LLAMA_EXAMPLE_COMMON)) {
return 1;
}
if (params.n_parallel == 1) {
LOG_TRC("%s: n_parallel == 1, enabling unified kv cache\n", __func__);
params.kv_unified = true;
}
if (params.n_predict < 0) {
params.n_predict = 16;
}
ggml_backend_load_all();
// Run the full save/load test suite (tests 1-5) for a single model.
// Returns true if all tests pass, false otherwise.
static bool run_save_load_tests_for_model(const std::string & model_path, const struct common_params & base_params) {
struct common_params params = base_params;
params.model.path = model_path;
auto llama_init = common_init_from_params(params, true);
auto * model = llama_init->model();
if (model == nullptr) {
LOG_ERR("%s: failed to init\n", __func__);
return 1;
LOG_ERR("%s: failed to init model '%s'\n", __func__, model_path.c_str());
return false;
}
GGML_ASSERT(llama_init->context() == nullptr);
@@ -411,30 +399,127 @@ int main(int argc, char ** argv) {
// Test 1: baseline (saves state to disk)
auto result_baseline = test_baseline(model, params, tokens);
if (result_baseline.empty()) {
return 1;
return false;
}
// Test 2: sequence removal isolation
if (!test_seq_rm_isolated(model, params, tokens)) {
return 1;
return false;
}
// Test 3: state load
if (!test_state_load(model, params, tokens, result_baseline)) {
return 1;
return false;
}
// Test 4: seq copy (host)
if (!test_seq_cp_host(model, params, tokens, result_baseline)) {
return 1;
return false;
}
// Test 5: seq copy (device)
if (!test_seq_cp_device(model, params, tokens, result_baseline)) {
return 1;
return false;
}
LOG("\nAll tests passed.\n");
return 0;
return true;
}
int main(int argc, char ** argv) {
std::setlocale(LC_NUMERIC, "C");
common_params params;
params.prompt = "";
params.n_batch = 100;
params.out_file = "dump_state.bin";
params.sampling.seed = 1234;
common_init();
// extract our own --models DIR option before handing the rest to the common arg parser
std::string models_dir;
std::vector<char *> filtered_argv;
filtered_argv.push_back(argv[0]);
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--models") == 0) {
if (i + 1 >= argc) {
LOG_ERR("%s: --models requires a directory argument\n", __func__);
return 1;
}
models_dir = argv[i + 1];
i++;
} else {
filtered_argv.push_back(argv[i]);
}
}
filtered_argv.push_back(nullptr);
const int fargc = (int)filtered_argv.size() - 1;
// in --models mode there is no single model; set a placeholder so the common parser's
// "--model is required" check passes (each model is set individually inside the loop)
if (!models_dir.empty()) {
params.model.path = models_dir;
}
if (!common_params_parse(fargc, filtered_argv.data(), params, LLAMA_EXAMPLE_COMMON)) {
return 1;
}
if (params.n_parallel == 1) {
LOG_TRC("%s: n_parallel == 1, enabling unified kv cache\n", __func__);
params.kv_unified = true;
}
if (params.n_predict < 0) {
params.n_predict = 16;
}
ggml_backend_load_all();
if (!models_dir.empty()) {
// run the suite over every dummy model in the directory
if (!std::filesystem::exists(models_dir) || !std::filesystem::is_directory(models_dir)) {
LOG_ERR("%s: models directory '%s' does not exist\n", __func__, models_dir.c_str());
return 1;
}
std::vector<std::string> models;
for (const auto & entry : std::filesystem::directory_iterator(models_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".gguf") {
models.push_back(entry.path().string());
}
}
std::sort(models.begin(), models.end());
if (models.empty()) {
LOG_ERR("%s: no .gguf models found in '%s'\n", __func__, models_dir.c_str());
return 1;
}
LOG_INF("%s: running save/load tests over %zu models in '%s'\n", __func__, models.size(), models_dir.c_str());
size_t n_pass = 0;
size_t n_fail = 0;
for (const auto & model_path : models) {
LOG("\n================================================================\n");
LOG_INF("%s: model %s\n", __func__, model_path.c_str());
if (run_save_load_tests_for_model(model_path, params)) {
n_pass++;
} else {
n_fail++;
}
}
LOG("\n================================================================\n");
LOG_INF("%s: summary: %zu passed, %zu failed (of %zu)\n", __func__, n_pass, n_fail, models.size());
return n_fail == 0 ? 0 : 1;
}
// single-model mode
return run_save_load_tests_for_model(params.model.path, params) ? 0 : 1;
}
+4 -1
View File
@@ -59,12 +59,14 @@
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
| `-lm, --load-mode MODE` | model loading mode (default: auto)<br/>- auto: mmap, unless a device does not support it<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)<br/>- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)<br/>- auto: on, but only for tensors larger than 4 GiB<br/>- off: always keep them resident<br/>(env: LLAMA_ARG_TENSOR_READ_LAZY) |
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
| `--list-devices` | print list of available devices and exit |
| `-ot, --override-tensor <tensor name pattern>=<buffer type>,...` | override tensor buffer type<br/>(env: LLAMA_ARG_OVERRIDE_TENSOR) |
| `-cmoe, --cpu-moe` | keep all Mixture of Experts (MoE) weights in the CPU<br/>(env: LLAMA_ARG_CPU_MOE) |
| `-ncmoe, --n-cpu-moe N` | keep the Mixture of Experts (MoE) weights of the first N layers in the CPU<br/>(env: LLAMA_ARG_N_CPU_MOE) |
| `-ncffn, --n-cpu-ffn N` | keep the dense FFN weights of the first N layers in the CPU<br/>(dense models; for MoE expert weights use --n-cpu-moe)<br/>(env: LLAMA_ARG_N_CPU_FFN) |
| `-ngl, --gpu-layers, --n-gpu-layers N` | max. number of layers to store in VRAM, either an exact number, 'auto', or 'all' (default: auto)<br/>(env: LLAMA_ARG_N_GPU_LAYERS) |
| `-sm, --split-mode {none,layer,row,tensor}` | how to split the model across multiple GPUs, one of:<br/>- none: use one GPU only<br/>- layer (default): split layers and KV across GPUs (pipelined)<br/>- row: split weight across GPUs by rows (parallelized)<br/>- tensor: split weights and KV across GPUs (parallelized, EXPERIMENTAL)<br/>(env: LLAMA_ARG_SPLIT_MODE) |
| `-ts, --tensor-split N0,N1,N2,...` | fraction of the model to offload to each GPU, comma-separated list of proportions, e.g. 3,1<br/>(env: LLAMA_ARG_TENSOR_SPLIT) |
@@ -154,7 +156,6 @@
| `-sysf, --system-prompt-file FNAME` | a file containing the system prompt (default: none) |
| `-r, --reverse-prompt PROMPT` | halt generation at PROMPT, return control in interactive mode |
| `-sp, --special` | special tokens output enabled (default: false) |
| `-cnv, --conversation, -no-cnv, --no-conversation` | whether to run in conversation mode:<br/>- does not print special tokens and suffix/prefix<br/>- interactive mode is also enabled<br/>(default: auto enabled if chat template is available) |
| `-st, --single-turn` | run conversation for a single turn only, then exit when done<br/>will not be interactive if first turn is predefined with --prompt<br/>(default: false) |
| `-mli, --multiline-input` | allows you to write or paste multiple lines without ending each in '\' |
| `--warmup, --no-warmup` | whether to perform warmup with an empty run (default: enabled) |
@@ -200,6 +201,8 @@
| `--spec-draft-n-cpu-moe, --spec-draft-ncmoe, -ncmoed, --n-cpu-moe-draft N` | keep the Mixture of Experts (MoE) weights of the first N layers in the CPU for the draft model<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_CPU_MOE) |
| `--spec-draft-n-max N` | number of tokens to draft for speculative decoding (default: 3)<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_MAX) |
| `--spec-draft-n-min N` | minimum number of draft tokens to use for speculative decoding (default: 0)<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_MIN) |
| `--spec-synth-len L` | target mean synthetic acceptance length, including the target token (benchmarking only)<br/>(env: LLAMA_ARG_SPEC_SYNTH_LEN) |
| `--spec-synth-rates P0,P1,...` | comma-separated unconditional per-position synthetic acceptance probabilities (benchmarking only)<br/>(env: LLAMA_ARG_SPEC_SYNTH_RATES) |
| `--spec-draft-p-split, --draft-p-split P` | speculative decoding split probability (default: 0.10)<br/>(env: LLAMA_ARG_SPEC_DRAFT_P_SPLIT) |
| `--spec-draft-p-min, --draft-p-min P` | minimum speculative decoding probability (greedy) (default: 0.00)<br/>(env: LLAMA_ARG_SPEC_DRAFT_P_MIN) |
| `--spec-draft-backend-sampling, --no-spec-draft-backend-sampling` | offload draft sampling to the backend (default: enabled)<br/>(env: LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING) |
+2
View File
@@ -142,12 +142,14 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
| `-lm, --load-mode MODE` | model loading mode (default: auto)<br/>- auto: mmap, unless a device does not support it<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)<br/>- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)<br/>- auto: on, but only for tensors larger than 4 GiB<br/>- off: always keep them resident<br/>(env: LLAMA_ARG_TENSOR_READ_LAZY) |
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
| `--list-devices` | print list of available devices and exit |
| `-ot, --override-tensor <tensor name pattern>=<buffer type>,...` | override tensor buffer type<br/>(env: LLAMA_ARG_OVERRIDE_TENSOR) |
| `-cmoe, --cpu-moe` | keep all Mixture of Experts (MoE) weights in the CPU<br/>(env: LLAMA_ARG_CPU_MOE) |
| `-ncmoe, --n-cpu-moe N` | keep the Mixture of Experts (MoE) weights of the first N layers in the CPU<br/>(env: LLAMA_ARG_N_CPU_MOE) |
| `-ncffn, --n-cpu-ffn N` | keep the dense FFN weights of the first N layers in the CPU<br/>(dense models; for MoE expert weights use --n-cpu-moe)<br/>(env: LLAMA_ARG_N_CPU_FFN) |
| `-ngl, --gpu-layers, --n-gpu-layers N` | max. number of layers to store in VRAM, either an exact number, 'auto', or 'all' (default: auto)<br/>(env: LLAMA_ARG_N_GPU_LAYERS) |
| `-sm, --split-mode {none,layer,row,tensor}` | how to split the model across multiple GPUs, one of:<br/>- none: use one GPU only<br/>- layer (default): split layers and KV across GPUs (pipelined)<br/>- row: split weight across GPUs by rows (parallelized)<br/>- tensor: split weights and KV across GPUs (parallelized, EXPERIMENTAL)<br/>(env: LLAMA_ARG_SPLIT_MODE) |
| `-ts, --tensor-split N0,N1,N2,...` | fraction of the model to offload to each GPU, comma-separated list of proportions, e.g. 3,1<br/>(env: LLAMA_ARG_TENSOR_SPLIT) |
+15 -2
View File
@@ -122,7 +122,7 @@ static bool try_parse_ftype(const std::string & ftype_str_in, llama_ftype & ftyp
static void usage(const char * executable) {
printf("usage: %s [--help] [--allow-requantize] [--leave-output-tensor] [--pure] [--imatrix] [--include-weights]\n", executable);
printf(" [--exclude-weights] [--output-tensor-type] [--token-embedding-type] [--tensor-type] [--tensor-type-file]\n");
printf(" [--prune-layers] [--keep-split] [--override-kv] [--dry-run]\n");
printf(" [--prune-layers] [--keep-split] [--override-kv] [--dry-run] [--max-buffer-size]\n");
printf(" model-f32.gguf [model-quant.gguf] type [nthreads]\n\n");
printf(" --allow-requantize\n");
printf(" allow requantizing tensors that have already been quantized\n");
@@ -161,7 +161,10 @@ static void usage(const char * executable) {
printf(" WARNING: this is an advanced option, use with care.\n");
printf(" --dry-run\n");
printf(" calculate and show the final quantization size without performing quantization\n");
printf(" example: llama-quantize --dry-run model-f32.gguf Q4_K\n\n");
printf(" example: llama-quantize --dry-run model-f32.gguf Q4_K\n");
printf(" --max-buffer-size MiB\n");
printf(" max amount of tensor rows kept in memory while quantizing one tensor (default: 8192)\n");
printf(" lower it to quantize models with very large tensors on a machine with little RAM\n\n");
printf("note: --include-weights and --exclude-weights cannot be used together\n\n");
printf("-----------------------------------------------------------------------------\n");
printf(" allowed quantization types\n");
@@ -467,6 +470,16 @@ int llama_quantize(int argc, char ** argv) {
}
} else if (strcmp(argv[arg_idx], "--keep-split") == 0) {
params.keep_split = true;
} else if (strcmp(argv[arg_idx], "--max-buffer-size") == 0) {
if (arg_idx == argc-1) {
usage(argv[0]);
}
const int mib = atoi(argv[++arg_idx]);
if (mib <= 0) {
fprintf(stderr, "%s: invalid --max-buffer-size '%s'\n", __func__, argv[arg_idx]);
return 1;
}
params.max_buf_size = (size_t) mib * 1024 * 1024;
} else {
usage(argv[0]);
}
+5
View File
@@ -76,12 +76,14 @@ For the full list of features, please refer to [server's changelog](https://gith
| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>(env: LLAMA_ARG_MMAP) |
| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available<br/>(env: LLAMA_ARG_DIO) |
| `-lm, --load-mode MODE` | model loading mode (default: auto)<br/>- auto: mmap, unless a device does not support it<br/>- none: no special loading mode<br/>- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)<br/>- mlock: force system to keep model in RAM rather than swapping or compressing<br/>- mmap+mlock: mmap + force system to keep model in RAM rather than swapping or compressing<br/>- dio: use DirectIO if available<br/><br/>(env: LLAMA_ARG_LOAD_MODE) |
| `--tensor-read-lazy MODE` | on-demand reading of certain tensors, for example per-layer embeddings (default: auto)<br/>- on: read the rows of such tensors from disk on demand instead of keeping them resident (requires mmap)<br/>- auto: on, but only for tensors larger than 4 GiB<br/>- off: always keep them resident<br/>(env: LLAMA_ARG_TENSOR_READ_LAZY) |
| `--numa TYPE` | attempt optimizations that help on some NUMA systems<br/>- distribute: spread execution evenly over all nodes<br/>- isolate: only spawn threads on CPUs on the node that execution started on<br/>- numactl: use the CPU map provided by numactl<br/>if run without this previously, it is recommended to drop the system page cache before using this<br/>see https://github.com/ggml-org/llama.cpp/issues/1437<br/>(env: LLAMA_ARG_NUMA) |
| `-dev, --device <dev1,dev2,..>` | comma-separated list of devices to use for offloading (none = don't offload)<br/>use --list-devices to see a list of available devices<br/>(env: LLAMA_ARG_DEVICE) |
| `--list-devices` | print list of available devices and exit |
| `-ot, --override-tensor <tensor name pattern>=<buffer type>,...` | override tensor buffer type<br/>(env: LLAMA_ARG_OVERRIDE_TENSOR) |
| `-cmoe, --cpu-moe` | keep all Mixture of Experts (MoE) weights in the CPU<br/>(env: LLAMA_ARG_CPU_MOE) |
| `-ncmoe, --n-cpu-moe N` | keep the Mixture of Experts (MoE) weights of the first N layers in the CPU<br/>(env: LLAMA_ARG_N_CPU_MOE) |
| `-ncffn, --n-cpu-ffn N` | keep the dense FFN weights of the first N layers in the CPU<br/>(dense models; for MoE expert weights use --n-cpu-moe)<br/>(env: LLAMA_ARG_N_CPU_FFN) |
| `-ngl, --gpu-layers, --n-gpu-layers N` | max. number of layers to store in VRAM, either an exact number, 'auto', or 'all' (default: auto)<br/>(env: LLAMA_ARG_N_GPU_LAYERS) |
| `-sm, --split-mode {none,layer,row,tensor}` | how to split the model across multiple GPUs, one of:<br/>- none: use one GPU only<br/>- layer (default): split layers and KV across GPUs (pipelined)<br/>- row: split weight across GPUs by rows (parallelized)<br/>- tensor: split weights and KV across GPUs (parallelized, EXPERIMENTAL)<br/>(env: LLAMA_ARG_SPLIT_MODE) |
| `-ts, --tensor-split N0,N1,N2,...` | fraction of the model to offload to each GPU, comma-separated list of proportions, e.g. 3,1<br/>(env: LLAMA_ARG_TENSOR_SPLIT) |
@@ -161,6 +163,7 @@ For the full list of features, please refer to [server's changelog](https://gith
| -------- | ----------- |
| `-lcs, --lookup-cache-static FNAME` | path to static lookup cache to use for lookup decoding (not updated by generation) |
| `-lcd, --lookup-cache-dynamic FNAME` | path to dynamic lookup cache to use for lookup decoding (updated by generation) |
| `--kv-unified-per-slot N` | context limit per parallel slot (default: unset, behavior unchanged).<br/>when set without -c/--ctx-size, the shared KV pool is sized to n_parallel*N<br/>(env: LLAMA_ARG_KV_UNIFIED_PER_SLOT) |
| `-ctxcp, --ctx-checkpoints, --swa-checkpoints N` | max number of context checkpoints to create per slot (default: 32)[(more info)](https://github.com/ggml-org/llama.cpp/pull/15293)<br/>(env: LLAMA_ARG_CTX_CHECKPOINTS) |
| `-cms, --checkpoint-min-step N` | minimum spacing between context checkpoints in tokens (default: 8192, 0 = no minimum)<br/>(env: LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT) |
| `-cram, --cache-ram N` | set the maximum cache size in MiB (default: 8192, -1 - no limit, 0 - disable)[(more info)](https://github.com/ggml-org/llama.cpp/pull/16391)<br/>(env: LLAMA_ARG_CACHE_RAM) |
@@ -259,6 +262,8 @@ For the full list of features, please refer to [server's changelog](https://gith
| `--spec-draft-n-cpu-moe, --spec-draft-ncmoe, -ncmoed, --n-cpu-moe-draft N` | keep the Mixture of Experts (MoE) weights of the first N layers in the CPU for the draft model<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_CPU_MOE) |
| `--spec-draft-n-max N` | number of tokens to draft for speculative decoding (default: 3)<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_MAX) |
| `--spec-draft-n-min N` | minimum number of draft tokens to use for speculative decoding (default: 0)<br/>(env: LLAMA_ARG_SPEC_DRAFT_N_MIN) |
| `--spec-synth-len L` | target mean synthetic acceptance length, including the target token (benchmarking only)<br/>(env: LLAMA_ARG_SPEC_SYNTH_LEN) |
| `--spec-synth-rates P0,P1,...` | comma-separated unconditional per-position synthetic acceptance probabilities (benchmarking only)<br/>(env: LLAMA_ARG_SPEC_SYNTH_RATES) |
| `--spec-draft-p-split, --draft-p-split P` | speculative decoding split probability (default: 0.10)<br/>(env: LLAMA_ARG_SPEC_DRAFT_P_SPLIT) |
| `--spec-draft-p-min, --draft-p-min P` | minimum speculative decoding probability (greedy) (default: 0.00)<br/>(env: LLAMA_ARG_SPEC_DRAFT_P_MIN) |
| `--spec-draft-backend-sampling, --no-spec-draft-backend-sampling` | offload draft sampling to the backend (default: enabled)<br/>(env: LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING) |
+105 -11
View File
@@ -23,6 +23,7 @@
#include <exception>
#include <memory>
#include <filesystem>
#include <random>
#include <utility>
#include <fstream>
@@ -51,6 +52,50 @@ static common_speculative_output_limits server_output_limits(const common_params
return result;
}
// synthetic draft verification for benchmarking - accept draft tokens at random instead of by match with the target
// on replay the draft was already accepted before a context checkpoint restore, so repeat the same decisions
static std::vector<llama_token> server_sample_and_accept_synth(
common_sampler * smpl,
llama_context * ctx,
const std::vector<int32_t> & idxs,
const llama_tokens & draft,
const std::vector<double> & synth_probs,
std::mt19937 & rng,
bool is_replay) {
GGML_ASSERT(idxs.size() == draft.size() + 1);
GGML_ASSERT(synth_probs.size() >= draft.size());
std::vector<llama_token> result;
result.reserve(idxs.size());
const llama_vocab * vocab = llama_model_get_vocab(llama_get_model(ctx));
std::uniform_real_distribution<double> dist(0.0, 1.0);
for (size_t i = 0; i < draft.size(); ++i) {
const llama_token id = common_sampler_sample(smpl, ctx, idxs[i]);
const bool accept = is_replay || dist(rng) < synth_probs[i];
// do not accept a drafted EOG token - it would end the generation early
// on replay the last token is from the target and can be EOG, so skip this check
if (accept && (is_replay || !llama_vocab_is_eog(vocab, draft[i]))) {
// synthetic draft tokens do not advance grammar or reasoning state
// the last replay token is from the target and must advance both
const bool is_replay_target = is_replay && i + 1 == draft.size();
common_sampler_accept(smpl, draft[i], is_replay_target);
result.push_back(draft[i]);
continue;
}
common_sampler_accept(smpl, id, true);
result.push_back(id);
return result;
}
const llama_token id = common_sampler_sample(smpl, ctx, idxs[draft.size()]);
common_sampler_accept(smpl, id, true);
result.push_back(id);
return result;
}
// state diagram: https://github.com/ggml-org/llama.cpp/pull/9283
enum slot_state {
SLOT_STATE_IDLE,
@@ -211,6 +256,7 @@ struct server_slot {
std::vector<int32_t> spec_i_batch;
common_prompt_checkpoint spec_ckpt;
bool spec_is_replay = false;
std::mt19937 spec_synth_rng;
// TODO: move members that belong to the task (such as `generated_text`, `has_new_line`) to task_results_state
// see https://github.com/ggml-org/llama.cpp/pull/18283#issuecomment-3710175837
@@ -1162,10 +1208,31 @@ private:
const int n_ctx_train = llama_model_n_ctx_train(model_tgt);
int n_ctx_slot = llama_n_ctx_seq(ctx_tgt);
if (n_ctx_slot > n_ctx_train) {
SRV_WRN("the slot context (%d) exceeds the training context of the model (%d) - capping\n", n_ctx_slot, n_ctx_train);
n_ctx_slot = n_ctx_train;
{
// note: the capping itself is done in n_ctx_slot(), here we only report it
const int n_ctx_seq = llama_n_ctx_seq(ctx_tgt);
if (params_base.kv_unified_per_slot > 0) {
if (n_ctx_seq > params_base.kv_unified_per_slot) {
SRV_INF("capping per-slot context (%d) to --kv-unified-per-slot (%d)\n",
n_ctx_seq, params_base.kv_unified_per_slot);
} else if (params_base.kv_unified_per_slot > n_ctx_seq) {
// cap is above the per-slot pool capacity, so it can never bind
SRV_WRN(
"--kv-unified-per-slot (%d) exceeds the per-slot pool capacity (%d) - cap has no effect, "
"slots are limited to %d (raise the KV pool with -c, or unset -c to size it to "
"n_parallel * kv_unified_per_slot)\n",
params_base.kv_unified_per_slot, n_ctx_seq, n_ctx_seq);
}
}
const int n_ctx_capped = params_base.kv_unified_per_slot > 0 ?
std::min(n_ctx_seq, params_base.kv_unified_per_slot) : n_ctx_seq;
if (n_ctx_capped > n_ctx_train) {
SRV_WRN("the slot context (%d) exceeds the training context of the model (%d) - capping\n",
n_ctx_capped, n_ctx_train);
}
}
slots.clear();
@@ -1181,7 +1248,7 @@ private:
// setup slots
SRV_INF("initializing, n_slots = %d, n_ctx_slot = %d, kv_unified = '%s'\n",
params_base.n_parallel, n_ctx_slot, params_base.kv_unified ? "true" : "false");
params_base.n_parallel, n_ctx_slot(), params_base.kv_unified ? "true" : "false");
// initialize slots
for (int i = 0; i < params_base.n_parallel; i++) {
@@ -1194,6 +1261,9 @@ private:
spec.reset(common_speculative_init(params_base.speculative, params_base.n_parallel));
} catch (const std::exception & e) {
SRV_ERR("failed to initialize speculative decoding context: %s\n", e.what());
if (params_base.speculative.has_synth()) {
return false;
}
}
}
@@ -1209,6 +1279,11 @@ private:
model_dft = nullptr;
}
if (!spec && params_base.speculative.has_synth()) {
SRV_ERR("%s", "synthetic acceptance requires an initialized speculative decoding context\n");
return false;
}
for (int i = 0; i < params_base.n_parallel; i++) {
server_slot & slot = slots[i];
@@ -1217,7 +1292,7 @@ private:
slot.ctx_dft = ctx_dft;
slot.mem.init(ctx_tgt, ctx_dft);
slot.spec = spec.get();
slot.n_ctx = n_ctx_slot;
slot.n_ctx = n_ctx_slot();
slot.mctx = mctx;
slot.prompt.tokens.has_mtmd = mctx != nullptr;
@@ -1717,6 +1792,13 @@ private:
SLT_TRC(slot, "sampler chain: %s\n", common_sampler_print(slot.smpl.get()).c_str());
SLT_TRC(slot, "sampler params: \n%s\n", task.params.sampling.print().c_str());
if (spec && !common_speculative_get_synth_probs(spec.get()).empty()) {
const uint32_t seed = task.params.sampling.seed == LLAMA_DEFAULT_SEED
? std::random_device{}()
: task.params.sampling.seed;
slot.spec_synth_rng.seed(seed);
}
} else {
slot.smpl.reset();
}
@@ -3802,7 +3884,12 @@ private:
common_sampler_ptr smpl_save(common_sampler_clone(slot.smpl.get()));
GGML_ASSERT(slot.spec_i_batch.size() == n_draft + 1);
auto accepted = common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft);
const auto & synth_probs = common_speculative_get_synth_probs(spec.get());
auto accepted = synth_probs.empty()
? common_sampler_sample_and_accept_n(slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft)
: server_sample_and_accept_synth(
slot.smpl.get(), slot.ctx_tgt, slot.spec_i_batch, slot.spec_draft,
synth_probs, slot.spec_synth_rng, slot.spec_is_replay);
slot.spec_i_batch.clear();
GGML_ASSERT(accepted.size() >= 1);
@@ -3868,7 +3955,7 @@ private:
auto & n_accepted_per_pos = slot.n_accepted_per_pos;
if (n_accepted_per_pos.empty()) {
n_accepted_per_pos.resize(common_speculative_n_max(&params_base.speculative), 0);
n_accepted_per_pos.resize(common_speculative_n_max(spec.get()), 0);
}
for (size_t i = 0; i < n_accepted && i < n_accepted_per_pos.size(); ++i) {
n_accepted_per_pos[i]++;
@@ -3909,8 +3996,15 @@ private:
});
}
int get_slot_n_ctx() {
return slots.back().n_ctx;
// context size of a single slot, capped by --kv-unified-per-slot and by the training context of the model
int n_ctx_slot() const {
int res = llama_n_ctx_seq(ctx_tgt);
if (params_base.kv_unified_per_slot > 0) {
res = std::min(res, params_base.kv_unified_per_slot);
}
return std::min(res, llama_model_n_ctx_train(model_tgt));
}
server_response_reader get_response_reader() {
@@ -4076,7 +4170,7 @@ server_context_meta server_context::get_meta() const {
/* has_inp_audio */ impl->chat_params.allow_audio,
/* has_inp_video */ impl->chat_params.allow_video,
/* json_ui_settings */ impl->json_ui_settings,
/* slot_n_ctx */ impl->get_slot_n_ctx(),
/* slot_n_ctx */ impl->n_ctx_slot(),
/* pooling_type */ llama_pooling_type(impl->ctx_tgt),
/* chat_params */ impl->chat_params,
+12
View File
@@ -157,6 +157,18 @@ int llama_server(common_params & params, int argc, char ** argv) {
}
}
// size the KV pool from --kv-unified-per-slot, unless the user pinned it with -c
// or with -c 0 for max context
const bool ctx_pool_auto_sized = params.kv_unified_per_slot > 0 &&
params.n_ctx == 0 &&
(uint32_t) params.fit_params_min_ctx != UINT32_MAX;
if (ctx_pool_auto_sized) {
params.n_ctx = params.n_parallel * params.kv_unified_per_slot;
SRV_INF("--kv-unified-per-slot: sizing KV pool to n_parallel * kv_unified_per_slot = %d * %d = %d\n", params.n_parallel,
params.kv_unified_per_slot, params.n_ctx);
}
// for consistency between server router mode and single-model mode, we set the same model name as alias
auto model_name = params.model.get_name();
if (params.model_alias.empty() && !model_name.empty()) {
@@ -52,6 +52,18 @@ def test_with_and_without_draft():
assert tokens_no_draft == tokens_draft
server.stop()
create_server()
assert server.spec_draft_n_max is not None
server.spec_synth_rates = [0.0] * server.spec_draft_n_max
server.start()
res = server.make_request("POST", "/completion", data=request)
assert res.status_code == 200
assert res.body["timings"]["draft_n"] > 0
assert res.body["timings"]["draft_n_accepted"] == 0
assert res.body["tokens"] == tokens_no_draft
def test_different_draft_min_draft_max():
global server
@@ -80,6 +92,66 @@ def test_different_draft_min_draft_max():
last_content = res.body["content"]
def test_synth_is_deterministic():
global server
assert server.spec_draft_n_max is not None
server.spec_synth_rates = [0.75 ** (i + 1) for i in range(server.spec_draft_n_max)]
server.start()
request = {
"prompt": "I believe the meaning of life is",
"temperature": 0.2,
"top_k": 5,
"seed": 4242,
"n_predict": 32,
}
responses = [server.make_request("POST", "/completion", data=request) for _ in range(2)]
for res in responses:
assert res.status_code == 200
assert res.body["timings"]["draft_n"] > 0
assert responses[0].body["timings"]["draft_n"] == responses[1].body["timings"]["draft_n"]
assert responses[0].body["timings"]["draft_n_accepted"] == responses[1].body["timings"]["draft_n_accepted"]
def test_synth_ignores_target_tokens():
global server
assert server.spec_draft_n_max is not None
server.spec_synth_rates = [1.0] * server.spec_draft_n_max
server.start()
res = server.make_request("POST", "/completion", data={
"prompt": "I believe the meaning of life is",
"temperature": 0.0,
"seed": 4242,
"n_predict": 32,
})
assert res.status_code == 200
assert res.body["timings"]["draft_n"] > 0
assert res.body["timings"]["draft_n_accepted"] == res.body["timings"]["draft_n"]
res = server.make_request("POST", "/completion", data={
"prompt": "I believe the meaning of life is",
"temperature": 0.0,
"seed": 4242,
"n_predict": 6,
"grammar": 'root ::= "a"{5,5}',
})
assert res.status_code == 200, res.body
res = server.make_request("POST", "/completion", data={
"prompt": "Respond with only: OK",
"temperature": 0.0,
"seed": 4242,
"n_predict": 64,
"ignore_eos": True,
})
assert res.status_code == 200, res.body
assert res.body["tokens_predicted"] == 64
assert res.body["stop_type"] == "limit"
def test_slot_ctx_not_exceeded():
global server
server.n_ctx = 256

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